Files
medical-mall/docs/数据库对接文档.md

442 KiB
Raw Blame History

医疗-consumer & 医疗-delivery 数据库对接文档

生成日期2026-06-01 适用范围:医疗-consumer消费者端+ 医疗-delivery配送端 数据库PostgreSQL (Supabase) 规范:所有用户侧表已启用 RLS行级安全全局/管理后台查询请走 RPCSECURITY DEFINER

目录

通用规范说明

软删除标准

本项目所有业务表默认采用软删除,标准字段如下:

  • deleted_at (timestamptz)删除时间NULL 表示未删除
  • deleted_by (uuid):删除操作人
  • created_at (timestamptz):创建时间,默认 now()
  • updated_at (timestamptz):更新时间,默认 now()

注意RLS 策略默认过滤 deleted_at IS NULL 的行,查询时无需手动加条件,但管理后台统计需视情况处理。

角色字段权威口径

  • 统一用户主表:public.ak_users
  • 角色唯一权威字段:ak_users.role,取值:customer(消费者)、merchant(商家)、delivery(配送员)、admin(管理员)、analytics(数据分析师)
  • 商城用户扩展档案:public.ml_user_profiles,与 ak_users 1:1 关系(user_id UNIQUE

状态机速查

状态域 字段名 关键取值 说明
订单流程 order_status 1=待付款, 2=待发货, 3=待收货, 4=已完成, 5=已取消, 6=退款中, 7=已退款 主状态
支付状态 payment_status 1=未支付, 2=已支付, 3=部分退款, 4=全额退款 支付线
物流状态 shipping_status 1=未发货, 2=已发货, 3=运输中, 4=已送达 物流线
配送任务 status 1=待接单, 2=已接单, 3=取货中, 4=配送中, 5=已送达, 6=配送失败 配送端

用户与权限

ml_user_profiles

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT uuid_generate_v4()
user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
status INTEGER DEFAULT 1 NOT NULL, -- 1:正常 2:冻结 3:注销 4:待审核
real_name VARCHAR(100) , -- 真实姓名
id_card VARCHAR(32) , -- 身份证号
business_license VARCHAR(100) , -- 营业执照号
credit_score INTEGER DEFAULT 100, -- 信用分数 0-1000
verification_status INTEGER DEFAULT 0, -- 认证状态0:未认证1:已认证2:认证失败
verification_data JSONB DEFAULT '{}', -- 认证相关数据
preferences JSONB DEFAULT '{}', -- 用户偏好设置
emergency_contact VARCHAR(200) , -- 紧急联系人
service_areas JSONB , -- 服务区域(配送员)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()

约束

  • CONSTRAINT chk_ml_user_status CHECK (status IN (1,2,3,4))
  • CONSTRAINT chk_ml_verification_status CHECK (verification_status IN (0,1,2))
  • CONSTRAINT chk_ml_credit_score CHECK (credit_score >= 0 AND credit_score <= 1000)

ml_user_addresses

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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))

ml_shopping_cart

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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)

ml_user_favorites

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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))

ml_browse_history

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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)

ml_search_history

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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()

ak_roles

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL UNIQUE, -- 角色名称 (如: 超级管理员)
code TEXT NOT NULL UNIQUE, -- 角色编码 (如: super_admin)
description TEXT , -- 角色描述
is_active BOOLEAN NOT NULL DEFAULT TRUE
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS:已启用,暂无显式策略(默认拒绝直接访问)


ak_permissions

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
parent_id UUID REFERENCES public.ak_permissions(id) ON DELETE CASCADE
name TEXT NOT NULL, -- 权限/菜单名称
code TEXT NOT NULL UNIQUE, -- 权限编码 (如: order_view)
type TEXT NOT NULL, -- 类型: menu(菜单), button(按钮/接口)
path TEXT , -- 前端路由路径 (仅针对 menu)
icon TEXT , -- 图标
sort_order INTEGER DEFAULT 0, -- 排序
is_visible BOOLEAN DEFAULT TRUE, -- 菜单是否在左侧可见
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS:已启用,暂无显式策略(默认拒绝直接访问)


ak_admin_roles

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
role_id UUID NOT NULL REFERENCES public.ak_roles(id) ON DELETE CASCADE
assigned_at TIMESTAMPTZ NOT NULL DEFAULT now()
assigned_by UUID REFERENCES public.ak_users(id)

约束

  • UNIQUE(user_id, role_id)

RLS:已启用,暂无显式策略(默认拒绝直接访问)


ak_role_permissions

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
role_id UUID NOT NULL REFERENCES public.ak_roles(id) ON DELETE CASCADE
permission_id UUID NOT NULL REFERENCES public.ak_permissions(id) ON DELETE CASCADE

约束

  • UNIQUE(role_id, permission_id)

RLS:已启用,暂无显式策略(默认拒绝直接访问)


ak_user_groups

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL
remark TEXT NULL
status INT NOT NULL DEFAULT 1, -- 1:启用, 0:禁用
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
deleted_at TIMESTAMPTZ NULL

约束

  • CONSTRAINT ak_user_groups_name_length CHECK (char_length(name) >= 1)

RLS:已启用,暂无显式策略(默认拒绝直接访问)

相关 RPC(最多展示 8 个):

  • rpc_admin_user_group_save(p_id UUID DEFAULT NULL, p_name TEXT, p_remark TEXT DEFAULT NULL, p_status INT DEFAULT 1) -> UUID
  • rpc_admin_user_group_set_status(p_id UUID, p_status INT) -> BOOLEAN

ak_user_labels

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL
color TEXT NULL
remark TEXT NULL
status INT NOT NULL DEFAULT 1
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
deleted_at TIMESTAMPTZ NULL

约束

  • CONSTRAINT ak_user_labels_name_length CHECK (char_length(name) >= 1)

RLS:已启用,暂无显式策略(默认拒绝直接访问)

相关 RPC(最多展示 8 个):

  • rpc_admin_user_label_save(p_id UUID DEFAULT NULL, p_name TEXT, p_color TEXT DEFAULT NULL, p_remark TEXT DEFAULT NULL, p_status...) -> UUID
  • rpc_admin_user_label_set_status(p_id UUID, p_status INT) -> BOOLEAN

ak_user_levels

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL
level_weight INT NOT NULL
min_experience INT NOT NULL DEFAULT 0
discount_percent INT NOT NULL DEFAULT 100
is_visible BOOLEAN NOT NULL DEFAULT TRUE
status INT NOT NULL DEFAULT 1
icon_url TEXT NULL
bg_image_url TEXT NULL
bg_style_json JSONB NULL
remark TEXT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
deleted_at TIMESTAMPTZ NULL

约束

  • CONSTRAINT ak_user_levels_level_weight_nonnegative CHECK (level_weight >= 0)
  • CONSTRAINT ak_user_levels_min_experience_nonnegative CHECK (min_experience >= 0)
  • CONSTRAINT ak_user_levels_discount_percent_range CHECK (discount_percent BETWEEN 1 AND 100)

RLS 策略

  • ak_user_levels_public_select_visible_active

相关 RPC(最多展示 8 个):

  • rpc_admin_user_level_save(p_id UUID DEFAULT NULL, p_name TEXT, p_level_weight INT, p_min_experience INT, p_discount_percent IN...) -> UUID
  • rpc_admin_user_level_set_status(p_id UUID, p_status INT) -> BOOLEAN
  • rpc_admin_user_level_set_visible(p_id UUID, p_is_visible BOOLEAN) -> BOOLEAN

ak_users

  • 所属端consumer / delivery
字段名 数据类型 约束/默认值 说明
id uuid primary key
username text
email text
gender text
birthday date
height_cm numeric
weight_kg numeric
bio text
avatar_url text
preferred_language text
health_goal text
service_address text
emergency_contact text
chronic_notes text
care_preference text
role text
school_id text
grade_id text
class_id text
created_at timestamptz default now()
updated_at timestamptz default now()

商品与类目

ml_categories

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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()

ml_brands

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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()

ml_products

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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_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))

相关 RPC(最多展示 8 个):

  • rpc_admin_product_stats(p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE) -> JSONB

ml_product_skus

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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))

ml_product_specs

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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()

ml_shops

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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))

ml_product_reviews

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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))

相关 RPC(最多展示 8 个):

  • rpc_admin_get_product_reviews(p_search_product text DEFAULT NULL, p_search_user text DEFAULT NULL, p_status integer DEFAULT NULL, ...) -> TABLE ( id uuid, product_id uuid, product_name text, product_image text, user_id uuid, username text, rating integer, content text, merchant_reply text, status integer, created_at timestamptz, total_count bigint )

ak_shipping_templates

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL, -- 模板名称
calc_method TEXT DEFAULT 'piece', -- 计费方式: piece(件数), weight(重量), volume(体积)
is_free_shipping BOOLEAN DEFAULT false, -- 是否包邮
sort_order INTEGER DEFAULT 0
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

medical_mall_categories

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id TEXT PRIMARY KEY
name TEXT NOT NULL
parent_id TEXT NULL REFERENCES public.medical_mall_categories(id)
level INTEGER NOT NULL CHECK (level IN (1, 2))
sort_order INTEGER NOT NULL DEFAULT 0
icon TEXT
image_url TEXT
description TEXT
scene TEXT NOT NULL DEFAULT 'medical_mall'
category_type TEXT NOT NULL DEFAULT 'normal'
compliance_type TEXT NOT NULL DEFAULT 'normal'
is_active BOOLEAN NOT NULL DEFAULT TRUE
deleted_at TIMESTAMPTZ NULL
deleted_by UUID NULL REFERENCES public.ak_users(id)
restored_at TIMESTAMPTZ NULL
restored_by UUID NULL REFERENCES public.ak_users(id)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

RLS 策略

  • medical_mall_categories_public_select_active

medical_mall_product_categories

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE
category_id TEXT NOT NULL REFERENCES public.medical_mall_categories(id)
is_primary BOOLEAN NOT NULL DEFAULT FALSE
sort_order INTEGER NOT NULL DEFAULT 0
deleted_at TIMESTAMPTZ NULL
deleted_by UUID NULL REFERENCES public.ak_users(id)
restored_at TIMESTAMPTZ NULL
restored_by UUID NULL REFERENCES public.ak_users(id)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

约束

  • UNIQUE (product_id, category_id)

RLS 策略

  • medical_mall_product_categories_public_select_active

订单

ml_orders

  • 所属端consumer / delivery
字段名 数据类型 约束/默认值 说明
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))

相关 RPC(最多展示 8 个):

  • rpc_admin_order_source_stats(p_start_time TIMESTAMPTZ, p_end_time TIMESTAMPTZ) -> JSONB
  • rpc_admin_order_stats(p_start_time TIMESTAMPTZ, p_end_time TIMESTAMPTZ) -> JSONB

ml_order_items

  • 所属端consumer / delivery
字段名 数据类型 约束/默认值 说明
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()

配送与物流

ml_delivery_drivers

  • 所属端consumer / delivery
字段名 数据类型 约束/默认值 说明
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))

ml_delivery_tasks

  • 所属端consumer / delivery
字段名 数据类型 约束/默认值 说明
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))

ml_delivery_staff

  • 所属端consumer / delivery
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
uid UUID REFERENCES public.ak_users(id) ON DELETE SET NULL, -- 关联用户(可选)
nickname TEXT NOT NULL, -- 配送员名称
avatar TEXT , -- 头像
phone TEXT NOT NULL, -- 手机号
status SMALLINT NOT NULL DEFAULT 1, -- 状态: 1-启用, 0-禁用
is_active BOOLEAN NOT NULL DEFAULT TRUE
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • delivery_staff_self_select
  • delivery_staff_self_update

相关 RPC(最多展示 8 个):

  • rpc_admin_delete_delivery_staff(p_id UUID) -> BOOLEAN
  • rpc_admin_get_delivery_staff_list(p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAULT 1, p_page_size IN...) -> JSONB
  • rpc_admin_get_delivery_staff_list(p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAULT 1, p_page_size IN...) -> JSONB
  • rpc_admin_save_delivery_staff(p_id UUID DEFAULT NULL, p_nickname TEXT DEFAULT NULL, p_avatar TEXT DEFAULT NULL, p_phone TEXT DEFAU...) -> UUID
  • rpc_admin_save_delivery_staff(p_id UUID DEFAULT NULL, p_uid UUID DEFAULT NULL, p_station_id UUID DEFAULT NULL, p_staff_no TEXT DEF...) -> UUID

ml_delivery_stations

  • 所属端consumer / delivery
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL, -- 提货点名称
phone TEXT NOT NULL, -- 联系电话
address TEXT NOT NULL, -- 详细地址
image TEXT , -- 门店图片
lng NUMERIC(10,7) , -- 经度
lat NUMERIC(10,7) , -- 纬度
status SMALLINT NOT NULL DEFAULT 1, -- 状态: 1-显示, 0-隐藏
sort_order INTEGER DEFAULT 0
business_hours JSONB , -- 营业时间 (如: {"start": "09:00", "end": "21:00"})
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • delivery_stations_select_active
  • delivery_stations_select_active

营销与促销

ml_coupon_templates

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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))

RLS 策略

  • ml_coupon_templates_merchant_policy
  • ml_coupon_templates_select_policy

分销与推广

ak_commission_logs

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
source_uid UUID REFERENCES public.ak_users(id) ON DELETE SET NULL
order_id UUID REFERENCES public.ml_orders(id) ON DELETE SET NULL
order_no VARCHAR(50)
amount DECIMAL(12,2) NOT NULL DEFAULT 0
status TEXT NOT NULL DEFAULT 'frozen', -- frozen/available/withdrawn/canceled
frozen_until TIMESTAMPTZ
remark TEXT
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

约束

  • CONSTRAINT chk_ak_commission_amount_nonneg CHECK (amount >= 0)
  • CONSTRAINT chk_ak_commission_status CHECK (status IN ('frozen','available','withdrawn','canceled'))

RLS 策略

  • commission_logs_select_policy

ak_distribution_agent_applications

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
division_uid UUID NOT NULL REFERENCES public.ak_distribution_divisions(uid)
agent_name TEXT NOT NULL
agent_phone TEXT NULL
proof_images JSONB NULL, -- 申请凭证图片列表
status TEXT NOT NULL DEFAULT 'pending', -- pending/approved/rejected
refusal_reason TEXT NULL
approved_at TIMESTAMPTZ NULL
approved_by UUID NULL REFERENCES public.ak_users(id)
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • dist_apply_user_policy

ak_distribution_agents

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
uid UUID PRIMARY KEY REFERENCES public.ak_users(id) ON DELETE CASCADE
division_uid UUID NOT NULL REFERENCES public.ak_distribution_divisions(uid), -- 所属事业部
name TEXT NOT NULL
commission_ratio NUMERIC(5,2) DEFAULT 0 CHECK (commission_ratio >= 0 AND commission_ratio <= 100)
is_enabled BOOLEAN DEFAULT TRUE
end_time TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
created_by UUID REFERENCES public.ak_users(id)
updated_by UUID REFERENCES public.ak_users(id)

RLS 策略

  • dist_agents_select_policy

ak_distribution_config

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id TEXT PRIMARY KEY DEFAULT 'global_config'
is_enabled BOOLEAN DEFAULT true, -- 分销启用
extract_type TEXT DEFAULT '2', -- 分销模式: 1指定, 2人人, 3满额
bind_type TEXT DEFAULT '2', -- 绑定关系: 1所有用户, 2新用户
store_brokerage_binding_status TEXT DEFAULT '1', -- 绑定模式: 1永久, 2有效期, 3临时
brokerage_poster_status TEXT , -- 分销海报图路径/URL
brokerage_level INTEGER DEFAULT 2, -- 分销层级: 1, 2
is_area_manager BOOLEAN DEFAULT true, -- 事业部开关
is_agent_apply BOOLEAN DEFAULT true, -- 代理商申请开关
is_commission_window BOOLEAN DEFAULT true, -- 佣金悬浮窗开关
is_self_brokerage BOOLEAN DEFAULT true, -- 自购返佣
is_member_brokerage BOOLEAN DEFAULT false, -- 购买会员返佣
brokerage_type TEXT DEFAULT '1', -- 返佣类型: 1价格, 2实付
is_promoter_brokerage BOOLEAN DEFAULT true, -- 推广用户返佣
promoter_brokerage_price DECIMAL(10,2) DEFAULT 2.00
promoter_brokerage_day_max DECIMAL(10,2) DEFAULT -1.00
store_brokerage_ratio DECIMAL(10,2) DEFAULT 20.00
store_brokerage_two_ratio DECIMAL(10,2) DEFAULT 2.00
extract_frozen_time INTEGER DEFAULT 1
user_extract_min_price DECIMAL(10,2) DEFAULT 1.00
extract_bank_list TEXT DEFAULT '中国银行'
extract_type_list TEXT[] DEFAULT ARRAY['bank', 'wechat', 'alipay']
wechat_extract_type TEXT DEFAULT '1'
alipay_extract_type TEXT DEFAULT '1'
user_extract_fee DECIMAL(10,2) DEFAULT 0.00
updated_at TIMESTAMPTZ DEFAULT now()
updated_by UUID REFERENCES auth.users(id)

RLS 策略

  • dist_config_select_policy

ak_distribution_division_applications

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
division_id UUID NOT NULL REFERENCES public.ak_distribution_divisions(id) ON DELETE CASCADE
name TEXT NOT NULL, -- 申请人填写的代理商名称
phone TEXT NOT NULL, -- 申请人联系电话
images JSONB DEFAULT '[]'::jsonb, -- 申请附件图片 (数组)
status INTEGER DEFAULT 1, -- 状态: 1待审核, 2已同意, 3已拒绝
admin_remark TEXT , -- 审核备注
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

ak_distribution_divisions

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
uid UUID PRIMARY KEY REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL
invite_code TEXT UNIQUE NOT NULL
commission_ratio NUMERIC(5,2) DEFAULT 0 CHECK (commission_ratio >= 0 AND commission_ratio <= 100)
is_enabled BOOLEAN DEFAULT TRUE
end_time TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
created_by UUID REFERENCES public.ak_users(id)
updated_by UUID REFERENCES public.ak_users(id)

RLS 策略

  • dist_divisions_select_policy

ak_distribution_level

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL, -- 等级名称
level INTEGER NOT NULL UNIQUE, -- 等级权重/数字如1, 2, 3
percent1 DECIMAL(10,2) DEFAULT 0, -- 一级分佣比例 (%)
percent2 DECIMAL(10,2) DEFAULT 0, -- 二级分佣比例 (%)
task_total INTEGER DEFAULT 0, -- 任务总数
task_finish INTEGER DEFAULT 0, -- 需完成数量(升级门槛)
is_visible BOOLEAN DEFAULT true, -- 是否显示
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • dist_level_select_policy

ak_promoter_relations

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
inviter_uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
bind_time TIMESTAMPTZ DEFAULT now()
created_at TIMESTAMPTZ DEFAULT now()

约束

  • CONSTRAINT chk_ak_promoter_relations_no_self CHECK (uid <> inviter_uid)
  • CONSTRAINT uq_ak_promoter_relations_uid UNIQUE (uid)

RLS 策略

  • promoter_relations_select_policy

财务

ml_extract

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
uid UUID NOT NULL REFERENCES public.ak_users(id)
real_name TEXT NULL, -- 提现人姓名快照
extract_type TEXT NOT NULL, -- 提现方式: alipay, wechat, bank
alipay_code TEXT NULL, -- 支付宝账号
wechat_code TEXT NULL, -- 微信账号
bank_code TEXT NULL, -- 银行卡号
bank_address TEXT NULL, -- 开户行地址
extract_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 申请提现金额
service_fee DECIMAL(12,2) NOT NULL DEFAULT 0, -- 提现手续费
balance DECIMAL(12,2) NOT NULL DEFAULT 0, -- 提现时的余额快照
status SMALLINT NOT NULL DEFAULT 0, -- 状态: 0:待审核, 1:已通过, -1:已驳回
refusal_reason TEXT NULL, -- 驳回原因
admin_id UUID NULL REFERENCES public.ak_users(id), -- 审核人ID
payment_time TIMESTAMPTZ NULL, -- 打款/到账时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • ml_extract_user_select

相关 RPC(最多展示 8 个):

  • rpc_admin_extract_list(p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_status SMALLINT DEFAULT NULL, p_start_ti...) -> JSONB
  • rpc_admin_extract_review(p_extract_id UUID, p_status SMALLINT, -- 1: 通过, -1: 驳回 p_refusal_reason TEXT DEFAULT NULL) -> VOID

ml_invoices

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
uid UUID NOT NULL REFERENCES public.ak_users(id)
order_no TEXT NOT NULL, -- 关联订单号
order_amount DECIMAL(12,2) NOT NULL, -- 订单金额
invoice_type SMALLINT NOT NULL DEFAULT 1, -- 1: 电子普通发票, 2: 增值税专用发票
header_type SMALLINT NOT NULL DEFAULT 1, -- 1: 个人, 2: 企业
header_name TEXT NOT NULL, -- 发票抬头
tax_id TEXT NULL, -- 企业税号
email TEXT NULL, -- 接收邮箱
remark TEXT NULL, -- 备注
status SMALLINT NOT NULL DEFAULT 0, -- 0: 待开票, 1: 已开票, -1: 已拒绝
refusal_reason TEXT NULL, -- 驳回原因
invoice_url TEXT NULL, -- 电子发票文件路径/URL
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • ml_invoices_user_select

ml_user_bill

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
uid UUID NOT NULL REFERENCES public.ak_users(id)
link_id TEXT NULL, -- 关联业务ID订单号、提现ID、充值ID等
pm SMALLINT NOT NULL DEFAULT 1, -- 0:支出, 1:收入
title TEXT NOT NULL, -- 流水标题(如:商品购买、充值、提现)
category TEXT NOT NULL, -- 业务大类balance-余额, integral-积分, brokerage-佣金)
type TEXT NOT NULL, -- 业务子类型recharge, extract, pay, refund, system_add, system_sub
number DECIMAL(12,2) NOT NULL DEFAULT 0, -- 变动金额
balance DECIMAL(12,2) NOT NULL DEFAULT 0, -- 变动后的余额快照
mark TEXT NULL, -- 备注
status SMALLINT NOT NULL DEFAULT 1, -- 状态1:有效, 0:无效/冲正)
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • ml_user_bill_user_select

相关 RPC(最多展示 8 个):

  • rpc_admin_user_bill_list(p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_category VARCHAR DEFAULT NULL, p_type VA...) -> JSONB

ml_user_recharge

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
uid UUID NOT NULL REFERENCES public.ak_users(id)
order_no TEXT UNIQUE NOT NULL, -- 充值订单号cz开头
recharge_type TEXT NOT NULL, -- 充值渠道: wechat, alipay, system (后台补单)
price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 实际充值金额
give_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 赠送金额
paid SMALLINT NOT NULL DEFAULT 0, -- 支付状态: 0:未支付, 1:已支付
pay_time TIMESTAMPTZ NULL, -- 支付时间
channel_trade_no TEXT NULL, -- 外部渠道流水号
status SMALLINT NOT NULL DEFAULT 1, -- 记录状态: 1:正常, 0:逻辑删除
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • ml_user_recharge_user_select

内容与装修

ml_article_categories

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL
icon TEXT NULL
sort INTEGER NOT NULL DEFAULT 0
status SMALLINT NOT NULL DEFAULT 1, -- 1: 启用, 0: 禁用
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • ml_article_categories_select_active

ml_articles

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
category_id UUID NOT NULL REFERENCES public.ml_article_categories(id)
title TEXT NOT NULL
author TEXT NULL
image TEXT NULL, -- 文章封面图
description TEXT NULL, -- 文章简介
content TEXT NOT NULL, -- 文章内容 (富文本)
status SMALLINT NOT NULL DEFAULT 0, -- 0: 未发布, 1: 已发布
views INTEGER NOT NULL DEFAULT 0, -- 浏览量
is_banner BOOLEAN NOT NULL DEFAULT FALSE, -- 是否展示在 banner
is_hot BOOLEAN NOT NULL DEFAULT FALSE, -- 是否热门
linked_product_id UUID NULL, -- 关联商品ID (可选)
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • ml_articles_select_published

相关 RPC(最多展示 8 个):

  • rpc_admin_article_save(p_id UUID DEFAULT NULL, p_category_id UUID DEFAULT NULL, p_title TEXT DEFAULT NULL, p_author TEXT DE...) -> UUID
  • rpc_admin_article_set_status(p_id UUID, p_status SMALLINT) -> BOOLEAN

ak_diy_pages

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL
type TEXT NOT NULL, -- home: 首页, topic: 专题页, user: 个人中心
config JSONB NOT NULL DEFAULT '{}'::jsonb, -- 核心布局配置 (组件列表及参数)
is_home BOOLEAN NOT NULL DEFAULT FALSE, -- 是否为生效首页
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
created_by UUID REFERENCES public.ak_users(id)
updated_by UUID REFERENCES public.ak_users(id)

RLS 策略

  • diy_pages_select_active

客服

ml_kefu_accounts

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
user_id UUID NOT NULL REFERENCES public.ak_users(id), -- 关联主用户表
nickname TEXT NOT NULL, -- 客服昵称
avatar TEXT NULL, -- 客服头像
status SMALLINT NOT NULL DEFAULT 1, -- 1:启用, 0:禁用
is_online BOOLEAN NOT NULL DEFAULT FALSE
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS:已启用,暂无显式策略(默认拒绝直接访问)

相关 RPC(最多展示 8 个):

  • rpc_admin_kefu_account_save(p_id UUID DEFAULT NULL, p_user_id UUID DEFAULT NULL, p_nickname TEXT DEFAULT NULL, p_avatar TEXT DEF...) -> UUID
  • rpc_admin_kefu_account_set_status(p_id UUID, p_status SMALLINT) -> BOOLEAN

ml_kefu_word_categories

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
name TEXT NOT NULL
sort INT NOT NULL DEFAULT 0
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS:已启用,暂无显式策略(默认拒绝直接访问)


ml_kefu_words

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
category_id UUID NOT NULL REFERENCES public.ml_kefu_word_categories(id) ON DELETE CASCADE
title TEXT NOT NULL
content TEXT NOT NULL
sort INT NOT NULL DEFAULT 0
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS:已启用,暂无显式策略(默认拒绝直接访问)

相关 RPC(最多展示 8 个):

  • rpc_admin_kefu_word_save(p_id UUID DEFAULT NULL, p_category_id UUID DEFAULT NULL, p_title TEXT DEFAULT NULL, p_content TEXT D...) -> UUID

ml_kefu_feedbacks

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
user_id UUID NULL REFERENCES public.ak_users(id), -- 允许匿名留言
nickname TEXT NULL
phone TEXT NULL
content TEXT NOT NULL
status SMALLINT NOT NULL DEFAULT 0, -- 0:未处理, 1:已处理
reply_content TEXT NULL, -- 管理员回复内容
processed_at TIMESTAMPTZ NULL, -- 处理时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS 策略

  • ml_kefu_feedbacks_user_insert
  • ml_kefu_feedbacks_user_select

ml_kefu_auto_replies

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
keyword TEXT NOT NULL
content TEXT NOT NULL
reply_type TEXT NOT NULL DEFAULT 'text', -- text, image
status SMALLINT NOT NULL DEFAULT 1, -- 1:开启, 0:关闭
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

RLS:已启用,暂无显式策略(默认拒绝直接访问)


系统配置

ml_system_configs

  • 所属端consumer / delivery
字段名 数据类型 约束/默认值 说明
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()

RLS 策略

  • system_configs_select_policy

相关 RPC(最多展示 8 个):

  • rpc_admin_system_config_save(p_key TEXT, p_value JSONB, p_description TEXT DEFAULT NULL) -> BOOLEAN

其他

ml_user_coupons

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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))

ml_regions

  • 所属端consumer
字段名 数据类型 约束/默认值 说明
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()

ak_seckill_activities

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
title TEXT NOT NULL, -- 活动标题
single_limit INTEGER DEFAULT 1, -- 单次限购
total_limit INTEGER DEFAULT 10, -- 总购买数量限制
product_count INTEGER DEFAULT 0, -- 包含商品数量
time_range TEXT NOT NULL, -- 活动时段 (如 "06:00-24:00")
start_date TIMESTAMPTZ NOT NULL, -- 开始日期
end_date TIMESTAMPTZ NOT NULL, -- 结束日期
status BOOLEAN DEFAULT true, -- 状态: true开启, false关闭
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • seckill_select_policy

ak_combination_activities

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
uid UUID NOT NULL REFERENCES public.ak_users(id), -- 开团团长
product_id UUID NOT NULL REFERENCES public.ml_products(id), -- 拼团商品
people INTEGER DEFAULT 2, -- 几人团
count_people INTEGER DEFAULT 1, -- 当前几人参加
start_time TIMESTAMPTZ DEFAULT now(), -- 开团时间
stop_time TIMESTAMPTZ NOT NULL, -- 结束时间
status TEXT NOT NULL DEFAULT 'ongoing', -- ongoing进行中, pending未完成, ended已成功
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

约束

  • CONSTRAINT chk_comb_status CHECK (status IN ('ongoing', 'pending', 'ended'))

RLS 策略

  • combination_select_policy

ak_marketing_bargains

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_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
title TEXT NOT NULL, -- 活动标题
min_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 砍价最低价
stock INTEGER DEFAULT 0, -- 活动库存
start_time TIMESTAMPTZ NOT NULL, -- 开始时间
stop_time TIMESTAMPTZ NOT NULL, -- 结束时间
status BOOLEAN DEFAULT true, -- 状态: true开启, false关闭
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • bargain_select_policy

ak_marketing_groupbuys

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_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
title TEXT NOT NULL, -- 活动标题
price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 团购价格
people INTEGER DEFAULT 2, -- 成团人数要求
stock INTEGER DEFAULT 0, -- 活动库存
start_time TIMESTAMPTZ NOT NULL, -- 开始时间
stop_time TIMESTAMPTZ NOT NULL, -- 结束时间
status BOOLEAN DEFAULT true, -- 状态: true开启, false关闭
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • groupbuy_select_policy

ak_marketing_live_products

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_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
live_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 直播专属价
stock INTEGER DEFAULT 0, -- 直播可用库存
audit_status INTEGER DEFAULT 1, -- 审核状态: 1待审核, 2审核通过, 3审核驳回
is_show BOOLEAN DEFAULT true, -- 是否在直播间显示
sort_order INTEGER DEFAULT 0
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

ak_marketing_lotteries

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL, -- 活动名称
type INTEGER DEFAULT 1, -- 活动类型: 1积分抽奖, 2订单评价, 3订单支付
start_time TIMESTAMPTZ NOT NULL, -- 开始时间
end_time TIMESTAMPTZ NOT NULL, -- 结束时间
is_open BOOLEAN DEFAULT true, -- 是否开启
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • lottery_select_policy

ak_marketing_lottery_prizes

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
lottery_id UUID NOT NULL REFERENCES public.ak_marketing_lotteries(id) ON DELETE CASCADE
name TEXT NOT NULL, -- 奖品名称
prize_type TEXT NOT NULL, -- 奖品类型: points, balance, coupon, physical
amount DECIMAL(12,2) DEFAULT 0, -- 奖励面值/数量
stock INTEGER DEFAULT 0, -- 奖品库存
probability DECIMAL(5,2) DEFAULT 0, -- 中奖概率 (0-100)
sort_order INTEGER DEFAULT 0
created_at TIMESTAMPTZ DEFAULT now()

ak_marketing_live_anchors

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
nickname TEXT NOT NULL, -- 主播昵称
wechat TEXT , -- 微信号
phone TEXT , -- 联系电话
avatar_url TEXT , -- 头像
status BOOLEAN DEFAULT true, -- 状态
created_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • live_anchors_select_policy

ak_marketing_live_rooms

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
anchor_id UUID REFERENCES public.ak_marketing_live_anchors(id) ON DELETE SET NULL
name TEXT NOT NULL, -- 直播间名称
background_url TEXT , -- 背景图
share_img_url TEXT , -- 分享图
start_time TIMESTAMPTZ NOT NULL, -- 开始时间
end_time TIMESTAMPTZ NOT NULL, -- 计划结束时间
sort INTEGER DEFAULT 0, -- 排序
type TEXT DEFAULT 'phone', -- 类型: phone手机直播等
like_enabled BOOLEAN DEFAULT true, -- 开启点赞
sale_enabled BOOLEAN DEFAULT true, -- 开启卖货
comment_enabled BOOLEAN DEFAULT true, -- 开启评论
is_show BOOLEAN DEFAULT true, -- 是否显示
live_status INTEGER DEFAULT 1, -- 1未开始, 2直播中, 3暂停, 4已结束
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • live_rooms_select_policy

ak_marketing_checkin_configs

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id TEXT PRIMARY KEY DEFAULT 'checkin_config'
merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE
is_open BOOLEAN DEFAULT true, -- 签到开关
mode TEXT DEFAULT 'none', -- 签到模式: none(无限制), week(周循环), month(月循环)
notice_enabled BOOLEAN DEFAULT false, -- 签到提醒开关
integral_reward INTEGER DEFAULT 10, -- 每日签到赠送积分
exp_reward INTEGER DEFAULT 1, -- 每日签到赠送经验
updated_at TIMESTAMPTZ DEFAULT now()
updated_by UUID REFERENCES auth.users(id)

ak_marketing_newcomer_config

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id TEXT PRIMARY KEY DEFAULT 'newcomer_config'
merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE
balance_reward DECIMAL(12,2) DEFAULT 0.00, -- 赠送余额
integral_reward INTEGER DEFAULT 0, -- 赠送积分
coupons_json JSONB DEFAULT '[]'::jsonb
updated_at TIMESTAMPTZ DEFAULT now()
updated_by UUID REFERENCES auth.users(id)

RLS 策略

  • newcomer_config_select_policy

ak_marketing_signin_logs

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
points INTEGER NOT NULL DEFAULT 0, -- 本次签到获得的积分
is_continuous_reward BOOLEAN DEFAULT false, -- 是否包含连续签到额外奖励
created_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • signin_logs_user_policy

ak_marketing_member_types

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL, -- 会员名 (如: 月卡, 年卡)
duration_days INTEGER DEFAULT 30, -- 有效期0表示永久
price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 原价
discount_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 优惠价/实际支付价
is_open BOOLEAN DEFAULT true, -- 是否开启
sort_order INTEGER DEFAULT 0, -- 排序
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • member_types_select_policy

ak_marketing_member_rights

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL, -- 权益名称
description TEXT , -- 权益简介
icon_url TEXT , -- 权益图标
is_show BOOLEAN DEFAULT true, -- 是否展示
sort_order INTEGER DEFAULT 0, -- 排序
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • member_rights_select_policy

ak_marketing_member_config

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id TEXT PRIMARY KEY DEFAULT 'member_config'
merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE
is_enabled BOOLEAN DEFAULT true, -- 是否开启付费会员功能
bg_img_url TEXT , -- 会员期内背景图
expire_bg_img_url TEXT , -- 会员到期背景图
rules_description TEXT , -- 会员规则说明文本
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • member_config_select_policy

ak_recharge_configs

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id TEXT PRIMARY KEY DEFAULT 'recharge_config'
merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE
balance_enabled BOOLEAN DEFAULT true, -- 余额功能是否启用
recharge_notice TEXT , -- 充值注意事项说明
mp_recharge_enabled BOOLEAN DEFAULT false, -- 小程序充值开关
min_recharge_amount DECIMAL(12,2) DEFAULT 0.01, -- 最低充值金额
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • recharge_config_select_policy

ak_recharge_quotas

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 售价(实际充值金额)
bonus_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 赠送金额
is_open BOOLEAN DEFAULT true, -- 是否可用
sort_order INTEGER DEFAULT 0, -- 排序
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

RLS 策略

  • recharge_quotas_select_policy

ak_signin_configs

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id TEXT PRIMARY KEY DEFAULT 'signin_config', -- 每个商家一个配置记录
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
is_enabled BOOLEAN DEFAULT true, -- 签到功能是否启用
daily_points INTEGER DEFAULT 10, -- 每日签到固定奖励积分
continuous_rewards JSONB DEFAULT '[]'::jsonb
rules_description TEXT DEFAULT '1.每日签到可获得积分奖励;\n2.连续签到满足天数可获得额外阶梯奖励;\n3.签到中断将重新从第一天开始计算。'
updated_at TIMESTAMPTZ DEFAULT now()
updated_by UUID REFERENCES auth.users(id)

约束

  • UNIQUE(merchant_id)

RLS 策略

  • signin_config_select_policy

ak_product_label_groups

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL
sort_order INTEGER DEFAULT 0
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

ak_product_labels

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
group_id UUID REFERENCES public.ak_product_label_groups(id) ON DELETE SET NULL
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL
is_active BOOLEAN DEFAULT true
show_in_mobile BOOLEAN DEFAULT true
sort_order INTEGER DEFAULT 0
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

ak_product_member_prices

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_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 NOT NULL REFERENCES public.ml_product_skus(id) ON DELETE CASCADE
level_id UUID NOT NULL REFERENCES public.ak_user_levels(id) ON DELETE CASCADE
member_price DECIMAL(12,2) NOT NULL CHECK (member_price >= 0)
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

约束

  • UNIQUE(merchant_id, sku_id, level_id)

ak_product_protections

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL
description TEXT NOT NULL DEFAULT ''
icon_url TEXT
sort_order INTEGER DEFAULT 0
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

ak_product_spec_templates

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL
specs TEXT NOT NULL DEFAULT ''
attrs TEXT NOT NULL DEFAULT ''
sort_order INTEGER DEFAULT 0
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

ak_product_param_templates

  • 所属端admin / 系统
字段名 数据类型 约束/默认值 说明
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE
name TEXT NOT NULL
sort_order INTEGER DEFAULT 0
params JSONB NOT NULL DEFAULT '[]'::jsonb
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()

完整对接 SQL 汇总

以下 SQL 按执行顺序排列Schema → RLS → RPC。请在新项目/新环境中按顺序执行。

1) 核心商城 Schemacomplete_mall_database.sql

-- =====================================================================================
-- 电商商城系统完整数据库设计(PostgreSQL + Supabase)
-- 表名前缀: ml_ (mall)
-- 复用主表 ak_users (用户主表)
-- 包含: 表结构、索引、触发器、RLS策略、视图、函数
-- =====================================================================================

-- =====================================================================================
-- 1. 基础配置和扩展
-- =====================================================================================

-- 启用必要的扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
CREATE EXTENSION IF NOT EXISTS "btree_gin";

-- =====================================================================================
-- 2. 用户扩展表
-- =====================================================================================

-- 商城用户扩展信息表
CREATE TABLE public.ml_user_profiles (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    status INTEGER DEFAULT 1 NOT NULL, -- 1:正常 2:冻结 3:注销 4:待审核
    real_name VARCHAR(100), -- 真实姓名
    id_card VARCHAR(32), -- 身份证号
    business_license VARCHAR(100), -- 营业执照号
    credit_score INTEGER DEFAULT 100, -- 信用分数 0-1000
    verification_status INTEGER DEFAULT 0, -- 认证状态0:未认证1:已认证2:认证失败
    verification_data JSONB DEFAULT '{}', -- 认证相关数据
    preferences JSONB DEFAULT '{}', -- 用户偏好设置
    emergency_contact VARCHAR(200), -- 紧急联系人
    service_areas JSONB, -- 服务区域(配送员)
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_user_status CHECK (status IN (1,2,3,4)),
    CONSTRAINT chk_ml_verification_status CHECK (verification_status IN (0,1,2)),
    CONSTRAINT chk_ml_credit_score CHECK (credit_score >= 0 AND credit_score <= 1000)
);

COMMENT ON TABLE public.ml_user_profiles IS '商城用户扩展信息表';
COMMENT ON COLUMN public.ml_user_profiles.status IS '用户状态1正常 2冻结 3注销 4待审核';

-- 用户地址表
CREATE TABLE public.ml_user_addresses (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    receiver_name VARCHAR(100) NOT NULL,
    receiver_phone VARCHAR(32) NOT NULL,
    province VARCHAR(100) NOT NULL,
    city VARCHAR(100) NOT NULL,
    district VARCHAR(100) NOT NULL,
    street VARCHAR(200),
    address_detail TEXT NOT NULL,
    postal_code VARCHAR(16),
    is_default BOOLEAN DEFAULT FALSE,
    label VARCHAR(50), -- home/office/school/other
    latitude DECIMAL(10,7),
    longitude DECIMAL(10,7),
    delivery_instructions TEXT,
    business_hours VARCHAR(100),
    status INTEGER DEFAULT 1, -- 1:正常 2:禁用
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_address_status CHECK (status IN (1,2))
);

COMMENT ON TABLE public.ml_user_addresses IS '用户地址表;

-- =====================================================================================
-- 3. 商品管理模块
-- =====================================================================================

-- 商品分类表
CREATE TABLE public.ml_categories (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
    parent_id UUID REFERENCES public.ml_categories(id),
    name VARCHAR(200) NOT NULL,
    slug VARCHAR(200) UNIQUE,
    description TEXT,
    icon_url TEXT,
    banner_url TEXT,
    sort_order INTEGER DEFAULT 0,
    level INTEGER DEFAULT 1,
    path TEXT[], -- 分类路径
    is_active BOOLEAN DEFAULT TRUE,
    seo_title VARCHAR(200),
    seo_description VARCHAR(500),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE public.ml_categories IS '商品分类表;

-- 品牌表
CREATE TABLE public.ml_brands (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
    name VARCHAR(200) NOT NULL,
    logo_url TEXT,
    description TEXT,
    website VARCHAR(500),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE public.ml_brands IS '品牌表;

-- 商品表
CREATE TABLE public.ml_products (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
    category_id UUID NOT NULL REFERENCES public.ml_categories(id),
    brand_id UUID REFERENCES public.ml_brands(id),
    product_code VARCHAR(100) UNIQUE NOT NULL,
    name VARCHAR(500) NOT NULL,
    subtitle VARCHAR(1000),
    description TEXT,
    main_image_url TEXT,
    image_urls JSONB DEFAULT '[]',
    video_urls JSONB DEFAULT '[]',
    
    -- 价格信息
    base_price DECIMAL(12,2) NOT NULL CHECK (base_price >= 0),
    market_price DECIMAL(12,2),
    cost_price DECIMAL(12,2),
    
    -- 库存信息
    total_stock INTEGER DEFAULT 0 CHECK (total_stock >= 0),
    available_stock INTEGER DEFAULT 0 CHECK (available_stock >= 0),
    min_order_qty INTEGER DEFAULT 1 CHECK (min_order_qty > 0),
    max_order_qty INTEGER,
    
    -- 基础属性
    weight DECIMAL(10,3),
    dimensions JSONB, -- {length, width, height}
    
    -- 状态
    status INTEGER DEFAULT 1, -- 1:上架 2:下架 3:草稿 4:删除
    is_featured BOOLEAN DEFAULT FALSE,
    is_new BOOLEAN DEFAULT FALSE,
    is_hot BOOLEAN DEFAULT FALSE,
    
    -- 统计
    view_count INTEGER DEFAULT 0,
    sale_count INTEGER DEFAULT 0,
    favorite_count INTEGER DEFAULT 0,
    rating_avg DECIMAL(3,2) DEFAULT 0.00 CHECK (rating_avg >= 0 AND rating_avg <= 5),
    rating_count INTEGER DEFAULT 0,
    
    -- SEO
    seo_title VARCHAR(200),
    seo_description VARCHAR(500),
    seo_keywords TEXT[],
    slug VARCHAR(200) UNIQUE,
    
    -- 其他
    tags TEXT[],
    attributes JSONB DEFAULT '{}',
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    published_at TIMESTAMP WITH TIME ZONE,
    
    CONSTRAINT chk_ml_product_status CHECK (status IN (1,2,3,4))
);

COMMENT ON TABLE public.ml_products IS '商品表;

-- 商品SKU表
CREATE TABLE public.ml_product_skus (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
    sku_code VARCHAR(100) UNIQUE NOT NULL,
    specifications JSONB DEFAULT '{}', -- 规格组合
    price DECIMAL(12,2) NOT NULL CHECK (price >= 0),
    market_price DECIMAL(12,2),
    cost_price DECIMAL(12,2),
    stock INTEGER DEFAULT 0 CHECK (stock >= 0),
    warning_stock INTEGER DEFAULT 10, -- 库存预警
    image_url TEXT,
    weight DECIMAL(10,3),
    status INTEGER DEFAULT 1, -- 1:正常 2:禁用
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_sku_status CHECK (status IN (1,2))
);

COMMENT ON TABLE public.ml_product_skus IS '商品SKU表;

-- 商品规格表
CREATE TABLE public.ml_product_specs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
    spec_name VARCHAR(100) NOT NULL, -- 规格名称:颜色、尺寸等
    spec_values JSONB NOT NULL DEFAULT '[]', -- 规格值数组
    sort_order INTEGER DEFAULT 0,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE public.ml_product_specs IS '商品规格表;

-- =====================================================================================
-- 4. 店铺管理模块
-- =====================================================================================

-- 店铺信息表
CREATE TABLE public.ml_shops (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
    merchant_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    shop_name VARCHAR(200) NOT NULL,
    shop_logo TEXT,
    shop_banner TEXT,
    description TEXT,
    business_license VARCHAR(100),
    contact_name VARCHAR(100),
    contact_phone VARCHAR(32),
    contact_email VARCHAR(200),
    address JSONB, -- 店铺地址信息
    business_hours JSONB, -- 营业时间
    
    -- 状态
    status INTEGER DEFAULT 1, -- 1:正常 2:暂停 3:关闭
    
    -- 统计
    product_count INTEGER DEFAULT 0,
    order_count INTEGER DEFAULT 0,
    rating_avg DECIMAL(3,2) DEFAULT 0.00,
    rating_count INTEGER DEFAULT 0,
    
    -- 认证信息
    verified_at TIMESTAMP WITH TIME ZONE,
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_shop_status CHECK (status IN (1,2,3))
);

COMMENT ON TABLE public.ml_shops IS '店铺信息表;

-- =====================================================================================
-- 5. 订单管理模块
-- =====================================================================================

-- 订单表
CREATE TABLE public.ml_orders (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
    order_no VARCHAR(50) UNIQUE NOT NULL,
    user_id UUID NOT NULL REFERENCES public.ak_users(id),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
    
    -- 金额信息
    product_amount DECIMAL(12,2) NOT NULL DEFAULT 0, -- 商品金额
    discount_amount DECIMAL(12,2) DEFAULT 0, -- 优惠金额
    shipping_fee DECIMAL(12,2) DEFAULT 0, -- 运费
    total_amount DECIMAL(12,2) NOT NULL, -- 总金额
    paid_amount DECIMAL(12,2) DEFAULT 0, -- 已付金额
    
    -- 地址信息
    shipping_address JSONB NOT NULL, -- 收货地址
    
    -- 状态信息
    order_status INTEGER DEFAULT 1, -- 1:待付款2:待发货3:待收货4:已完成5:已取消6:退款中 7:已退款
    payment_status INTEGER DEFAULT 1, -- 1:未付款2:已付款3:部分退款4:全额退款
    shipping_status INTEGER DEFAULT 1, -- 1:未发货2:已发货3:运输中4:已送达
    
    -- 时间信息
    paid_at TIMESTAMP WITH TIME ZONE,
    shipped_at TIMESTAMP WITH TIME ZONE,
    delivered_at TIMESTAMP WITH TIME ZONE,
    completed_at TIMESTAMP WITH TIME ZONE,
    
    -- 其他信息
    remark TEXT, -- 买家备注
    merchant_memo TEXT, -- 商家备注
    cancel_reason TEXT, -- 取消原因
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_order_status CHECK (order_status IN (1,2,3,4,5,6,7)),
    CONSTRAINT chk_ml_payment_status CHECK (payment_status IN (1,2,3,4)),
    CONSTRAINT chk_ml_shipping_status CHECK (shipping_status IN (1,2,3,4))
);

COMMENT ON TABLE public.ml_orders IS '订单表;

-- 订单商品表
CREATE TABLE public.ml_order_items (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    order_id UUID NOT NULL REFERENCES public.ml_orders(id) ON DELETE CASCADE,
    product_id UUID NOT NULL REFERENCES public.ml_products(id),
    sku_id UUID REFERENCES public.ml_product_skus(id),
    product_name VARCHAR(500) NOT NULL,
    sku_name VARCHAR(500),
    specifications JSONB DEFAULT '{}',
    image_url TEXT,
    price DECIMAL(12,2) NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    total_amount DECIMAL(12,2) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE public.ml_order_items IS '订单商品表;

-- =====================================================================================
-- 6. 购物车表
-- =====================================================================================

-- 购物车表
CREATE TABLE public.ml_shopping_cart (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
    sku_id UUID REFERENCES public.ml_product_skus(id) ON DELETE CASCADE,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    selected BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    UNIQUE(user_id, product_id, sku_id)
);

COMMENT ON TABLE public.ml_shopping_cart IS '购物车表';

-- =====================================================================================
-- 7. 营销管理模块
-- =====================================================================================

-- 优惠券模板表
CREATE TABLE public.ml_coupon_templates (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    cid SERIAL UNIQUE NOT NULL, -- SEO友好的自增ID
    merchant_id UUID REFERENCES public.ak_users(id), -- NULL表示平台券
    name VARCHAR(200) NOT NULL,
    description TEXT,
    coupon_type INTEGER NOT NULL, -- 1:满减券2:折扣券3:免运费券
    discount_type INTEGER NOT NULL, -- 1:固定金额 2:百分比
    discount_value DECIMAL(12,2) NOT NULL, -- 优惠值
    min_order_amount DECIMAL(12,2) DEFAULT 0, -- 最低订单金额
    max_discount_amount DECIMAL(12,2), -- 最大优惠金额
    total_quantity INTEGER, -- 总发放数量
    per_user_limit INTEGER DEFAULT 1, -- 每用户限领数量
    usage_limit INTEGER DEFAULT 1, -- 每张券使用次数限制
    
    -- 适用范围
    applicable_products JSONB DEFAULT '[]', -- 适用商品ID数组
    applicable_categories JSONB DEFAULT '[]', -- 适用分类ID数组
    
    -- 时间限制
    start_time TIMESTAMP WITH TIME ZONE NOT NULL,
    end_time TIMESTAMP WITH TIME ZONE NOT NULL,
    
    status INTEGER DEFAULT 1, -- 1:正常 2:暂停 3:已结束
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_coupon_type CHECK (coupon_type IN (1,2,3)),
    CONSTRAINT chk_ml_discount_type CHECK (discount_type IN (1,2)),
    CONSTRAINT chk_ml_coupon_status CHECK (status IN (1,2,3))
);

COMMENT ON TABLE public.ml_coupon_templates IS '优惠券模板表';

-- 用户优惠券表
CREATE TABLE public.ml_user_coupons (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    template_id UUID NOT NULL REFERENCES public.ml_coupon_templates(id),
    coupon_code VARCHAR(50) UNIQUE NOT NULL,
    status INTEGER DEFAULT 1, -- 1:未使用2:已使用3:已过期
    used_at TIMESTAMP WITH TIME ZONE,
    order_id UUID REFERENCES public.ml_orders(id),
    received_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    expire_at TIMESTAMP WITH TIME ZONE NOT NULL,
    
    CONSTRAINT chk_ml_user_coupon_status CHECK (status IN (1,2,3))
);

COMMENT ON TABLE public.ml_user_coupons IS '用户优惠券表';

-- =====================================================================================
-- 8. 配送管理表
-- =====================================================================================

-- 配送员信息表
CREATE TABLE public.ml_delivery_drivers (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    real_name VARCHAR(100) NOT NULL,
    id_card VARCHAR(32) NOT NULL,
    driver_license VARCHAR(50),
    vehicle_type INTEGER, -- 1:电动车2:摩托车3:汽车
    vehicle_number VARCHAR(20),
    service_areas JSONB DEFAULT '[]', -- 服务区域
    work_status INTEGER DEFAULT 1, -- 1:在线 2:忙碌 3:离线
    current_lat DECIMAL(10,7),
    current_lng DECIMAL(10,7),
    rating_avg DECIMAL(3,2) DEFAULT 0.00,
    rating_count INTEGER DEFAULT 0,
    order_count INTEGER DEFAULT 0,
    status INTEGER DEFAULT 1, -- 1:正常 2:暂停 3:离职
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_driver_vehicle_type CHECK (vehicle_type IN (1,2,3)),
    CONSTRAINT chk_ml_driver_work_status CHECK (work_status IN (1,2,3)),
    CONSTRAINT chk_ml_driver_status CHECK (status IN (1,2,3))
);

COMMENT ON TABLE public.ml_delivery_drivers IS '配送员信息表;

-- 配送任务表
CREATE TABLE public.ml_delivery_tasks (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    order_id UUID UNIQUE NOT NULL REFERENCES public.ml_orders(id),
    driver_id UUID REFERENCES public.ml_delivery_drivers(id),
    pickup_address JSONB NOT NULL, -- 取货地址
    delivery_address JSONB NOT NULL, -- 配送地址
    distance DECIMAL(8,2), -- 配送距离km)
    estimated_time INTEGER, -- 预计配送时间分钟)
    delivery_fee DECIMAL(10,2) NOT NULL DEFAULT 0,
    
    status INTEGER DEFAULT 1, -- 1:待接单2:已接单3:取货中4:配送中 5:已送达 6:配送失败
    
    -- 时间记录
    assigned_at TIMESTAMP WITH TIME ZONE,
    picked_at TIMESTAMP WITH TIME ZONE,
    delivered_at TIMESTAMP WITH TIME ZONE,
    
    -- 其他信息
    delivery_code VARCHAR(10), -- 取货中
    remark TEXT,
    failure_reason TEXT,
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_delivery_status CHECK (status IN (1,2,3,4,5,6))
);

COMMENT ON TABLE public.ml_delivery_tasks IS '配送任务表';

-- =====================================================================================
-- 9. 评价管理模块
-- =====================================================================================

-- 商品评价表
CREATE TABLE public.ml_product_reviews (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    order_id UUID NOT NULL REFERENCES public.ml_orders(id),
    order_item_id UUID NOT NULL REFERENCES public.ml_order_items(id),
    user_id UUID NOT NULL REFERENCES public.ak_users(id),
    product_id UUID NOT NULL REFERENCES public.ml_products(id),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id),
    
    rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
    content TEXT,
    images JSONB DEFAULT '[]', -- 评价图片
    is_anonymous BOOLEAN DEFAULT FALSE,
    
    -- 商家回复
    merchant_reply TEXT,
    merchant_replied_at TIMESTAMP WITH TIME ZONE,
    
    status INTEGER DEFAULT 1, -- 1:正常 2:已删除3:已隐藏
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT chk_ml_review_status CHECK (status IN (1,2,3))
);

COMMENT ON TABLE public.ml_product_reviews IS '商品评价表;

-- =====================================================================================
-- 10. 用户行为模块
-- =====================================================================================

-- 用户收藏表
CREATE TABLE public.ml_user_favorites (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    target_type INTEGER NOT NULL, -- 1:商品 2:店铺
    target_id UUID NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    UNIQUE(user_id, target_type, target_id),
    CONSTRAINT chk_ml_favorite_type CHECK (target_type IN (1,2))
);

COMMENT ON TABLE public.ml_user_favorites IS '用户收藏表;

-- 用户浏览历史表
CREATE TABLE public.ml_browse_history (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
    browse_duration INTEGER DEFAULT 0, -- 浏览时长(秒)
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    UNIQUE(user_id, product_id)
);

COMMENT ON TABLE public.ml_browse_history IS '用户浏览历史表;

-- 搜索记录表
CREATE TABLE public.ml_search_history (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES public.ak_users(id) ON DELETE CASCADE,
    keyword VARCHAR(200) NOT NULL,
    result_count INTEGER DEFAULT 0,
    ip_address INET,
    user_agent TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE public.ml_search_history IS '搜索记录表;

-- =====================================================================================
-- 11. 系统配置表
-- =====================================================================================

-- 系统配置表
CREATE TABLE public.ml_system_configs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    config_key VARCHAR(100) UNIQUE NOT NULL,
    config_value JSONB,
    description TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE public.ml_system_configs IS '系统配置表;

-- 地区表如果需要独立的地区管理)
CREATE TABLE public.ml_regions (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    parent_id UUID REFERENCES public.ml_regions(id),
    name VARCHAR(100) NOT NULL,
    code VARCHAR(20),
    level INTEGER NOT NULL, -- 1:省份 2:城市 3:区县 4:街道
    sort_order INTEGER DEFAULT 0,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE public.ml_regions IS '地区表;

-- =====================================================================================
-- 12. 创建索引
-- =====================================================================================

-- 用户扩展表索引
CREATE INDEX idx_ml_user_profiles_user_id ON public.ml_user_profiles(user_id);

CREATE INDEX idx_ml_user_profiles_status ON public.ml_user_profiles(status);

-- 分类表索引
CREATE INDEX idx_ml_categories_cid ON public.ml_categories(cid);
CREATE INDEX idx_ml_categories_parent ON public.ml_categories(parent_id);
CREATE INDEX idx_ml_categories_slug ON public.ml_categories(slug);
CREATE INDEX idx_ml_categories_level ON public.ml_categories(level, sort_order);

-- 品牌表索引
CREATE INDEX idx_ml_brands_cid ON public.ml_brands(cid);
CREATE INDEX idx_ml_brands_name ON public.ml_brands(name);

-- 地址表索引
CREATE INDEX idx_ml_user_addresses_user_id ON public.ml_user_addresses(user_id);
CREATE INDEX idx_ml_user_addresses_default ON public.ml_user_addresses(user_id, is_default);
CREATE INDEX idx_ml_user_addresses_location ON public.ml_user_addresses(city, district);

-- 商品表索引
CREATE INDEX idx_ml_products_cid ON public.ml_products(cid);
CREATE INDEX idx_ml_products_merchant ON public.ml_products(merchant_id, status);
CREATE INDEX idx_ml_products_category ON public.ml_products(category_id, status);
CREATE INDEX idx_ml_products_status ON public.ml_products(status, created_at DESC);
CREATE INDEX idx_ml_products_featured ON public.ml_products(is_featured, status);
CREATE INDEX idx_ml_products_price ON public.ml_products(base_price);
CREATE INDEX idx_ml_products_rating ON public.ml_products(rating_avg DESC, rating_count DESC);
CREATE INDEX idx_ml_products_sale_count ON public.ml_products(sale_count DESC);
CREATE INDEX idx_ml_products_tags ON public.ml_products USING GIN(tags);
CREATE INDEX idx_ml_products_slug ON public.ml_products(slug);

-- 店铺表索引
CREATE INDEX idx_ml_shops_cid ON public.ml_shops(cid);
CREATE INDEX idx_ml_shops_merchant ON public.ml_shops(merchant_id);

-- SKU表索引
CREATE INDEX idx_ml_product_skus_product ON public.ml_product_skus(product_id);
CREATE INDEX idx_ml_product_skus_code ON public.ml_product_skus(sku_code);

-- 订单表索引
CREATE INDEX idx_ml_orders_cid ON public.ml_orders(cid);
CREATE INDEX idx_ml_orders_user ON public.ml_orders(user_id, created_at DESC);
CREATE INDEX idx_ml_orders_merchant ON public.ml_orders(merchant_id, created_at DESC);
CREATE INDEX idx_ml_orders_status ON public.ml_orders(order_status, created_at DESC);
CREATE INDEX idx_ml_orders_no ON public.ml_orders(order_no);

-- 订单商品表索引
CREATE INDEX idx_ml_order_items_order ON public.ml_order_items(order_id);
CREATE INDEX idx_ml_order_items_product ON public.ml_order_items(product_id);

-- 购物车表索引
CREATE INDEX idx_ml_shopping_cart_user ON public.ml_shopping_cart(user_id);

-- 优惠券模板表索引
CREATE INDEX idx_ml_coupon_templates_cid ON public.ml_coupon_templates(cid);
CREATE INDEX idx_ml_coupon_templates_merchant ON public.ml_coupon_templates(merchant_id);

-- 优惠券表索引
CREATE INDEX idx_ml_user_coupons_user ON public.ml_user_coupons(user_id, status);
CREATE INDEX idx_ml_user_coupons_code ON public.ml_user_coupons(coupon_code);

-- 收藏表索引
CREATE INDEX idx_ml_user_favorites_user ON public.ml_user_favorites(user_id, target_type);
CREATE INDEX idx_ml_user_favorites_target ON public.ml_user_favorites(target_type, target_id);

-- 浏览历史索引
CREATE INDEX idx_ml_browse_history_user ON public.ml_browse_history(user_id, created_at DESC);
CREATE INDEX idx_ml_browse_history_product ON public.ml_browse_history(product_id);

-- =====================================================================================
-- 13. 触发器函数
-- =====================================================================================

-- 自动更新 updated_at 字段的函数
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 为需要的表创建 updated_at 触发器
CREATE TRIGGER trigger_ml_user_profiles_updated_at
    BEFORE UPDATE ON public.ml_user_profiles
    FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();

CREATE TRIGGER trigger_ml_user_addresses_updated_at
    BEFORE UPDATE ON public.ml_user_addresses
    FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();

CREATE TRIGGER trigger_ml_products_updated_at
    BEFORE UPDATE ON public.ml_products
    FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();

CREATE TRIGGER trigger_ml_product_skus_updated_at
    BEFORE UPDATE ON public.ml_product_skus
    FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();

CREATE TRIGGER trigger_ml_shops_updated_at
    BEFORE UPDATE ON public.ml_shops
    FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();

CREATE TRIGGER trigger_ml_orders_updated_at
    BEFORE UPDATE ON public.ml_orders
    FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();

CREATE TRIGGER trigger_ml_shopping_cart_updated_at
    BEFORE UPDATE ON public.ml_shopping_cart
    FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();

-- 确保每个用户只有一个默认地址的触发器
CREATE OR REPLACE FUNCTION public.ensure_single_default_address()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.is_default = TRUE THEN
        UPDATE public.ml_user_addresses 
        SET is_default = FALSE 
        WHERE user_id = NEW.user_id AND id != NEW.id;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_ml_single_default_address
    BEFORE INSERT OR UPDATE ON public.ml_user_addresses
    FOR EACH ROW EXECUTE FUNCTION public.ensure_single_default_address();

-- 商品库存更新触发器
CREATE OR REPLACE FUNCTION public.update_product_stock()
RETURNS TRIGGER AS $$
BEGIN
    -- 更新商品总库存
    UPDATE public.ml_products 
    SET 
        total_stock = (
            SELECT COALESCE(SUM(stock), 0) 
            FROM public.ml_product_skus 
            WHERE product_id = NEW.product_id AND status = 1
        ),
        available_stock = (
            SELECT COALESCE(SUM(stock), 0) 
            FROM public.ml_product_skus 
            WHERE product_id = NEW.product_id AND status = 1
        )
    WHERE id = NEW.product_id;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_ml_update_product_stock
    AFTER INSERT OR UPDATE OR DELETE ON public.ml_product_skus
    FOR EACH ROW EXECUTE FUNCTION public.update_product_stock();

-- 订单状态变更时的处理
CREATE OR REPLACE FUNCTION public.handle_order_status_change()
RETURNS TRIGGER AS $$
BEGIN
    -- 如果订单状态变为已付款
    IF NEW.order_status = 2 AND OLD.order_status = 1 THEN
        NEW.paid_at = NOW();
    END IF;
    
    -- 如果订单状态变为已发货
    IF NEW.order_status = 3 AND OLD.order_status = 2 THEN
        NEW.shipped_at = NOW();
    END IF;
    
    -- 如果订单状态变为已完成
    IF NEW.order_status = 4 AND OLD.order_status = 3 THEN
        NEW.delivered_at = NOW();
        NEW.completed_at = NOW();
        
        -- 更新商品销量
        UPDATE public.ml_products 
        SET sale_count = sale_count + (
            SELECT SUM(quantity) 
            FROM public.ml_order_items 
            WHERE order_id = NEW.id
        ) 
        WHERE id IN (
            SELECT product_id 
            FROM public.ml_order_items 
            WHERE order_id = NEW.id
        );
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_ml_order_status_change
    BEFORE UPDATE ON public.ml_orders
    FOR EACH ROW EXECUTE FUNCTION public.handle_order_status_change();

-- =====================================================================================
-- 14. 实用函数
-- =====================================================================================

-- 生成订单号的函数
CREATE OR REPLACE FUNCTION public.generate_order_no()
RETURNS TEXT AS $$
DECLARE
    order_no TEXT;
BEGIN
    order_no := 'ML' || TO_CHAR(NOW(), 'YYYYMMDD') || LPAD(NEXTVAL('ml_order_seq')::TEXT, 6, '0');
    RETURN order_no;
END;
$$ LANGUAGE plpgsql;

-- 创建订单序列
CREATE SEQUENCE IF NOT EXISTS public.ml_order_seq START 1;

-- 生成优惠券码的函数
CREATE OR REPLACE FUNCTION public.generate_coupon_code()
RETURNS TEXT AS $$
DECLARE
    code TEXT;
    chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    result TEXT := '';
    i INTEGER;
BEGIN
    FOR i IN 1..8 LOOP
        result := result || substr(chars, (random() * length(chars))::integer + 1, 1);
    END LOOP;
    RETURN 'CP' || result;
END;
$$ LANGUAGE plpgsql;

-- 获取用户默认地址
CREATE OR REPLACE FUNCTION public.get_user_default_address(p_user_id UUID)
RETURNS TABLE (
    id UUID,
    receiver_name VARCHAR,
    receiver_phone VARCHAR,
    full_address TEXT,
    latitude DECIMAL,
    longitude DECIMAL
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        a.id,
        a.receiver_name,
        a.receiver_phone,
        (a.province || ' ' || a.city || ' ' || a.district || ' ' || a.address_detail) as full_address,
        a.latitude,
        a.longitude
    FROM public.ml_user_addresses a
    WHERE a.user_id = p_user_id AND a.is_default = TRUE AND a.status = 1
    LIMIT 1;
END;
$$ LANGUAGE plpgsql;

-- 检查用户是否为认证商家
CREATE OR REPLACE FUNCTION public.is_verified_merchant(p_user_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
    result BOOLEAN := FALSE;
BEGIN
    SELECT (u.role = 'merchant' AND p.verification_status = 1) INTO result
    FROM public.ml_user_profiles p
    JOIN public.ak_users u ON p.user_id = u.id
    WHERE p.user_id = p_user_id;
    
    RETURN COALESCE(result, FALSE);
END;
$$ LANGUAGE plpgsql;

-- 计算购物车总金额
CREATE OR REPLACE FUNCTION public.calculate_cart_total(p_user_id UUID)
RETURNS DECIMAL AS $$
DECLARE
    total_amount DECIMAL := 0;
BEGIN
    SELECT COALESCE(SUM(s.price * c.quantity), 0) INTO total_amount
    FROM public.ml_shopping_cart c
    LEFT JOIN public.ml_product_skus s ON c.sku_id = s.id
    LEFT JOIN public.ml_products p ON c.product_id = p.id
    WHERE c.user_id = p_user_id 
      AND c.selected = TRUE
      AND p.status = 1 
      AND (s.id IS NULL OR s.status = 1);
    
    RETURN total_amount;
END;
$$ LANGUAGE plpgsql;

-- 获取商品可用库存
CREATE OR REPLACE FUNCTION public.get_product_available_stock(p_product_id UUID, p_sku_id UUID DEFAULT NULL)
RETURNS INTEGER AS $$
DECLARE
    stock_count INTEGER := 0;
BEGIN
    IF p_sku_id IS NOT NULL THEN
        -- 获取特定SKU库存
        SELECT COALESCE(stock, 0) INTO stock_count
        FROM public.ml_product_skus
        WHERE id = p_sku_id AND product_id = p_product_id AND status = 1;
    ELSE
        -- 获取商品总库存
        SELECT COALESCE(available_stock, 0) INTO stock_count
        FROM public.ml_products
        WHERE id = p_product_id AND status = 1;
    END IF;
    
    RETURN stock_count;
END;
$$ LANGUAGE plpgsql;

-- =====================================================================================
-- 15. 创建视图
-- =====================================================================================

-- 商城用户完整信息视图
CREATE OR REPLACE VIEW public.ml_users_view AS
SELECT 
    u.id,
    u.username,
    u.email,
    u.phone,
    u.avatar_url,
    u.gender,
    u.birthday,
    u.bio,
    u.created_at as user_created_at,
    u.updated_at as user_updated_at,
    u.role,
    p.status,
    p.real_name,
    p.credit_score,
    p.verification_status,
    p.created_at as profile_created_at,
    p.updated_at as profile_updated_at,
    CASE 
        WHEN u.role = 'customer' THEN '消费者'
        WHEN u.role = 'merchant' THEN '商家'
        WHEN u.role = 'delivery' THEN '配送员'
        WHEN u.role = 'service' THEN '客服'
        WHEN u.role = 'admin' THEN '管理员'
        ELSE '未知'
    END as role_name
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles p ON u.id = p.user_id;

COMMENT ON VIEW public.ml_users_view IS '商城用户完整信息视图';

-- 商品详情视图
CREATE OR REPLACE VIEW public.ml_products_detail_view AS
SELECT 
    p.*,
    c.cid as category_cid,
    c.name as category_name,
    c.path as category_path,
    b.cid as brand_cid,
    b.name as brand_name,
    s.cid as shop_cid,
    s.shop_name,
    u.username as merchant_name,
    CASE 
        WHEN p.status = 1 THEN '上架'
        WHEN p.status = 2 THEN '下架'
        WHEN p.status = 3 THEN '草稿'
        WHEN p.status = 4 THEN '删除'
        ELSE '未知'
    END as status_name
FROM public.ml_products p
LEFT JOIN public.ml_categories c ON p.category_id = c.id
LEFT JOIN public.ml_brands b ON p.brand_id = b.id
LEFT JOIN public.ml_shops s ON p.merchant_id = s.merchant_id
LEFT JOIN public.ak_users u ON p.merchant_id = u.id;

COMMENT ON VIEW public.ml_products_detail_view IS '商品详情视图';

-- 订单详情视图
CREATE OR REPLACE VIEW public.ml_orders_detail_view AS
SELECT 
    o.*,
    u.username as customer_name,
    u.phone as customer_phone,
    m.username as merchant_name,
    s.shop_name,
    CASE 
        WHEN o.order_status = 1 THEN '待付款
        WHEN o.order_status = 2 THEN '待发货
        WHEN o.order_status = 3 THEN '待收货
        WHEN o.order_status = 4 THEN '已完成
        WHEN o.order_status = 5 THEN '已取消
        WHEN o.order_status = 6 THEN '退款中'
        WHEN o.order_status = 7 THEN '已退款
        ELSE '未知'
    END as order_status_name,
    CASE 
        WHEN o.payment_status = 1 THEN '未付款
        WHEN o.payment_status = 2 THEN '已付款
        WHEN o.payment_status = 3 THEN '部分退款
        WHEN o.payment_status = 4 THEN '全额退款
        ELSE '未知'
    END as payment_status_name
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
LEFT JOIN public.ak_users m ON o.merchant_id = m.id
LEFT JOIN public.ml_shops s ON o.merchant_id = s.merchant_id;

COMMENT ON VIEW public.ml_orders_detail_view IS '订单详情视图';

-- =====================================================================================
-- 16. RLS (Row Level Security) 策略
-- =====================================================================================

-- 启用 RLS
ALTER TABLE public.ml_user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_user_addresses ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_shopping_cart ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_user_favorites ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_browse_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_user_coupons ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_products ENABLE ROW LEVEL SECURITY;

-- 用户只能访问自己的数据
CREATE POLICY ml_user_profiles_select_policy ON public.ml_user_profiles
    FOR SELECT USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_profiles_insert_policy ON public.ml_user_profiles
    FOR INSERT WITH CHECK (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_profiles_update_policy ON public.ml_user_profiles
    FOR UPDATE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_profiles_delete_policy ON public.ml_user_profiles
    FOR DELETE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_addresses_select_policy ON public.ml_user_addresses
    FOR SELECT USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_addresses_insert_policy ON public.ml_user_addresses
    FOR INSERT WITH CHECK (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_addresses_update_policy ON public.ml_user_addresses
    FOR UPDATE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_addresses_delete_policy ON public.ml_user_addresses
    FOR DELETE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_shopping_cart_select_policy ON public.ml_shopping_cart
    FOR SELECT USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_shopping_cart_insert_policy ON public.ml_shopping_cart
    FOR INSERT WITH CHECK (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_shopping_cart_update_policy ON public.ml_shopping_cart
    FOR UPDATE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_shopping_cart_delete_policy ON public.ml_shopping_cart
    FOR DELETE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_favorites_select_policy ON public.ml_user_favorites
    FOR SELECT USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_favorites_insert_policy ON public.ml_user_favorites
    FOR INSERT WITH CHECK (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_favorites_update_policy ON public.ml_user_favorites
    FOR UPDATE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_favorites_delete_policy ON public.ml_user_favorites
    FOR DELETE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_browse_history_select_policy ON public.ml_browse_history
    FOR SELECT USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_browse_history_insert_policy ON public.ml_browse_history
    FOR INSERT WITH CHECK (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_browse_history_update_policy ON public.ml_browse_history
    FOR UPDATE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_browse_history_delete_policy ON public.ml_browse_history
    FOR DELETE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_coupons_select_policy ON public.ml_user_coupons
    FOR SELECT USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_coupons_insert_policy ON public.ml_user_coupons
    FOR INSERT WITH CHECK (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_coupons_update_policy ON public.ml_user_coupons
    FOR UPDATE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

CREATE POLICY ml_user_coupons_delete_policy ON public.ml_user_coupons
    FOR DELETE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = user_id)
    );

-- 订单策略:用户可以查看自己的订单,商家可以查看自己店铺的订单
CREATE POLICY ml_orders_select_policy ON public.ml_orders
    FOR SELECT USING (
        auth.uid() IN (
            SELECT auth_id FROM public.ak_users WHERE id IN (user_id, merchant_id)
        )
    );

CREATE POLICY ml_orders_insert_policy ON public.ml_orders
    FOR INSERT WITH CHECK (
        auth.uid() IN (
            SELECT auth_id FROM public.ak_users WHERE id IN (user_id, merchant_id)
        )
    );

CREATE POLICY ml_orders_update_policy ON public.ml_orders
    FOR UPDATE USING (
        auth.uid() IN (
            SELECT auth_id FROM public.ak_users WHERE id IN (user_id, merchant_id)
        )
    );

CREATE POLICY ml_orders_delete_policy ON public.ml_orders
    FOR DELETE USING (
        auth.uid() IN (
            SELECT auth_id FROM public.ak_users WHERE id IN (user_id, merchant_id)
        )
    );

-- 商品策略:所有人可以查看上架商品,商家只能管理自己的商品
CREATE POLICY ml_products_select_policy ON public.ml_products
    FOR SELECT USING (status = 1);

CREATE POLICY ml_products_insert_policy ON public.ml_products
    FOR INSERT WITH CHECK (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = merchant_id)
    );

CREATE POLICY ml_products_update_policy ON public.ml_products
    FOR UPDATE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = merchant_id)
    );

CREATE POLICY ml_products_delete_policy ON public.ml_products
    FOR DELETE USING (
        auth.uid() = (SELECT auth_id FROM public.ak_users WHERE id = merchant_id)
    );

-- =====================================================================================
-- 17. 初始化数据
-- =====================================================================================

-- 插入系统配置
INSERT INTO public.ml_system_configs (config_key, config_value, description) VALUES
('shipping_fee', '{"default": 10, "free_threshold": 88}', '配送费配置'),
('platform_commission', '{"rate": 0.05}', '平台佣金配置'),
('coupon_settings', '{"max_per_user": 10}', '优惠券设置),
('order_auto_confirm_days', '7', '订单自动确认天数');

-- 插入默认分类
INSERT INTO public.ml_categories (id, name, slug, level, path) VALUES
(uuid_generate_v4(), '数码电器', 'digital', 1, ARRAY['数码电器']),
(uuid_generate_v4(), '服装鞋帽', 'fashion', 1, ARRAY['服装鞋帽']),
(uuid_generate_v4(), '家居用品', 'home', 1, ARRAY['家居用品']),
(uuid_generate_v4(), '食品饮料', 'food', 1, ARRAY['食品饮料']),
(uuid_generate_v4(), '美妆护肤', 'beauty', 1, ARRAY['美妆护肤']);

-- 为现有 ak_users 用户创建默认商城档案
INSERT INTO public.ml_user_profiles (user_id, status)
SELECT 
    id,
    1  -- 默认状态正常
FROM public.ak_users
WHERE id NOT IN (SELECT user_id FROM public.ml_user_profiles WHERE user_id IS NOT NULL);

-- =====================================================================================
-- 18. 完成提示
-- =====================================================================================

DO $$
BEGIN
    RAISE NOTICE '=======================================================';
    RAISE NOTICE '商城数据库创建完成!';
    RAISE NOTICE '=======================================================';
    RAISE NOTICE '已创建表数量: 20+ 张表';
    RAISE NOTICE '已创建索引 30+ 个索引;
    RAISE NOTICE '已创建触发器: 8 个触发器';
    RAISE NOTICE '已创建函数 10+ 个函数;
    RAISE NOTICE '已创建视图 3 个视图;
    RAISE NOTICE '已设置RLS策略: 多个策略';
    RAISE NOTICE '已为现有用户创建默认档案';
    RAISE NOTICE '=======================================================';
    RAISE NOTICE '表名前缀: ml_';
    RAISE NOTICE '复用主表 ak_users';
    RAISE NOTICE '兼容: Supabase';
    RAISE NOTICE '=======================================================';
END $$;

-- =====================================================================================
-- SEO 优化相关函数
-- =====================================================================================

-- 根据 cid 获取商品信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_product_by_cid(p_cid INTEGER)
RETURNS TABLE (
    id UUID,
    cid INTEGER,
    name VARCHAR,
    slug VARCHAR,
    description TEXT,
    main_image_url TEXT,
    base_price DECIMAL,
    rating_avg DECIMAL,
    sale_count INTEGER,
    category_name VARCHAR,
    brand_name VARCHAR,
    shop_name VARCHAR
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        p.id,
        p.cid,
        p.name,
        p.slug,
        p.description,
        p.main_image_url,
        p.base_price,
        p.rating_avg,
        p.sale_count,
        c.name as category_name,
        b.name as brand_name,
        s.shop_name
    FROM public.ml_products p
    LEFT JOIN public.ml_categories c ON p.category_id = c.id
    LEFT JOIN public.ml_brands b ON p.brand_id = b.id
    LEFT JOIN public.ml_shops s ON p.merchant_id = s.merchant_id
    WHERE p.cid = p_cid AND p.status = 1;
END;
$$ LANGUAGE plpgsql;

-- 根据 cid 获取分类信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_category_by_cid(p_cid INTEGER)
RETURNS TABLE (
    id UUID,
    cid INTEGER,
    name VARCHAR,
    slug VARCHAR,
    description TEXT,
    icon_url TEXT,
    path TEXT[]
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        c.id,
        c.cid,
        c.name,
        c.slug,
        c.description,
        c.icon_url,
        c.path
    FROM public.ml_categories c
    WHERE c.cid = p_cid AND c.is_active = TRUE;
END;
$$ LANGUAGE plpgsql;

-- 根据 cid 获取品牌信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_brand_by_cid(p_cid INTEGER)
RETURNS TABLE (
    id UUID,
    cid INTEGER,
    name VARCHAR,
    logo_url TEXT,
    description TEXT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        b.id,
        b.cid,
        b.name,
        b.logo_url,
        b.description
    FROM public.ml_brands b
    WHERE b.cid = p_cid AND b.is_active = TRUE;
END;
$$ LANGUAGE plpgsql;

-- 根据 cid 获取店铺信息 (SEO 友好)
CREATE OR REPLACE FUNCTION public.get_shop_by_cid(p_cid INTEGER)
RETURNS TABLE (
    id UUID,
    cid INTEGER,
    shop_name VARCHAR,
    description TEXT,
    shop_logo TEXT,
    rating_avg DECIMAL,
    product_count INTEGER
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        s.id,
        s.cid,
        s.shop_name,
        s.description,
        s.shop_logo,
        s.rating_avg,
        s.product_count
    FROM public.ml_shops s
    WHERE s.cid = p_cid AND s.status = 1;
END;
$$ LANGUAGE plpgsql;

-- 生成 SEO 友好的 URL 路径
CREATE OR REPLACE FUNCTION public.generate_seo_url(
    p_type VARCHAR, -- 'product', 'category', 'brand', 'shop'
    p_cid INTEGER,
    p_slug VARCHAR DEFAULT NULL
)
RETURNS TEXT AS $$
DECLARE
    url_path TEXT;
BEGIN
    CASE p_type
        WHEN 'product' THEN
            url_path := '/product/' || p_cid;
            IF p_slug IS NOT NULL THEN
                url_path := url_path || '/' || p_slug;
            END IF;
        WHEN 'category' THEN
            url_path := '/category/' || p_cid;
            IF p_slug IS NOT NULL THEN
                url_path := url_path || '/' || p_slug;
            END IF;
        WHEN 'brand' THEN
            url_path := '/brand/' || p_cid;
            IF p_slug IS NOT NULL THEN
                url_path := url_path || '/' || p_slug;
            END IF;
        WHEN 'shop' THEN
            url_path := '/shop/' || p_cid;
            IF p_slug IS NOT NULL THEN
                url_path := url_path || '/' || p_slug;
            END IF;
        ELSE
            url_path := '/' || p_type || '/' || p_cid;
    END CASE;
    
    RETURN url_path;
END;
$$ LANGUAGE plpgsql;

-- 批量更新 slug 字段(用于现有数据)
CREATE OR REPLACE FUNCTION public.update_seo_slugs()
RETURNS VOID AS $$
BEGIN
    -- 更新商品 slug
    UPDATE public.ml_products 
    SET slug = LOWER(REGEXP_REPLACE(name, '[^a-zA-Z0-9\u4e00-\u9fa5]+', '-', 'g'))
    WHERE slug IS NULL OR slug = '';
    
    -- 更新分类 slug  
    UPDATE public.ml_categories 
    SET slug = LOWER(REGEXP_REPLACE(name, '[^a-zA-Z0-9\u4e00-\u9fa5]+', '-', 'g'))
    WHERE slug IS NULL OR slug = '';
    
    RAISE NOTICE 'SEO slugs updated successfully';
END;
$$ LANGUAGE plpgsql;

2) 扩展模块 Schemadocs/sql/10_schema/

-- =====================================================================================
-- Migration: 全量软删除 (Soft Delete) 基础设施补齐
-- 位置docs/sql/10_schema/99_soft_delete_migration_v1.sql
-- 对象类型ALTER TABLE
-- 说明:为所有核心业务表补齐 deleted_at, deleted_by, restored_at, restored_by 字段
--      涵盖:权限、配置、内容、装修、物流、分销、财务、客服、营销、商品、用户、订单
-- =====================================================================================

DO $$
DECLARE
    -- 需补齐软删除字段的业务表全量清单
    t_names TEXT[] := ARRAY[
        -- 1. 系统与权限 (Auth/System)
        'ak_roles', 'ak_permissions', 'ak_admin_roles', 'ak_role_permissions', 'ml_system_configs',
        
        -- 2. 内容与装修 (CMS/Decoration)
        'ml_articles', 'ml_article_categories', 'ak_diy_pages',
        
        -- 3. 物流资源 (Delivery)
        'ml_delivery_staff', 'ml_delivery_stations', 'ak_shipping_templates',
        
        -- 4. 分销体系 (Distribution)
        'ak_distribution_agents', 'ak_distribution_divisions', 
        'ak_distribution_agent_applications', 'ak_distribution_division_applications',
        'ak_promoter_relations', 'ak_commission_logs', 'ak_distribution_level', 'ak_distribution_config',
        
        -- 5. 财务管理 (Finance)
        'ml_extract', 'ml_invoices', 'ml_user_bill', 'ml_user_recharge',
        
        -- 6. 客服系统 (Kefu)
        'ml_kefu_accounts', 'ml_kefu_words', 'ml_kefu_word_categories', 'ml_kefu_auto_replies', 'ml_kefu_feedbacks',
        
        -- 7. 营销活动 (Marketing)
        'ak_advanced_marketing', 'ak_bargain_groupbuy', 'ak_live_products', 'ak_lottery_live',
        'ak_marketing_checkin_configs', 'ak_marketing_newcomer_config', 'ak_marketing_signin_logs',
        'ak_member_management', 'ak_recharge_management', 'ak_signin_configs',
        
        -- 8. 商品中心 (Product)
        'ml_products', 'ml_product_skus', 'ml_categories', 
        'ak_product_labels', 'ak_product_member_prices', 'ak_product_protections', 'ak_product_templates',
        
        -- 9. 用户管理 (User)
        'ak_user_labels', 'ak_user_groups', 'ak_user_levels', 'ak_users',
        
        -- 10. 订单中心 (Order)
        'ml_orders'
    ];
    t_name TEXT;
BEGIN
    FOREACH t_name IN ARRAY t_names LOOP
        -- 检查表是否存在
        IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = t_name) THEN
            
            -- 1. 增加 deleted_at 字段
            IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = t_name AND column_name = 'deleted_at') THEN
                EXECUTE format('ALTER TABLE public.%I ADD COLUMN deleted_at TIMESTAMPTZ DEFAULT NULL', t_name);
            END IF;

            -- 2. 增加 deleted_by 字段
            IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = t_name AND column_name = 'deleted_by') THEN
                EXECUTE format('ALTER TABLE public.%I ADD COLUMN deleted_by UUID REFERENCES public.ak_users(id) ON DELETE SET NULL', t_name);
            END IF;

            -- 3. 增加 restored_at 字段
            IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = t_name AND column_name = 'restored_at') THEN
                EXECUTE format('ALTER TABLE public.%I ADD COLUMN restored_at TIMESTAMPTZ DEFAULT NULL', t_name);
            END IF;

            -- 4. 增加 restored_by 字段
            IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = t_name AND column_name = 'restored_by') THEN
                EXECUTE format('ALTER TABLE public.%I ADD COLUMN restored_by UUID REFERENCES public.ak_users(id) ON DELETE SET NULL', t_name);
            END IF;

            -- 5. 建立软删除索引
            EXECUTE format('CREATE INDEX IF NOT EXISTS %I ON public.%I (deleted_at) WHERE deleted_at IS NULL', 'idx_' || t_name || '_soft_delete', t_name);
            
        END IF;
    END LOOP;
END $$;
-- =====================================================================================
-- Schema: 系统配置表
-- 位置docs/sql/10_schema/admin/ml_system_configs_v1.sql
-- 对象类型TABLE
-- 版本v1
-- 说明:统一存储系统、应用、维护等模块的 Key-Value 配置项
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ml_system_configs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    config_key TEXT UNIQUE NOT NULL,
    config_value JSONB NOT NULL DEFAULT '{}'::jsonb,
    description TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 索引
CREATE INDEX IF NOT EXISTS idx_system_configs_key ON public.ml_system_configs (config_key);

-- 注释
COMMENT ON TABLE public.ml_system_configs IS '系统全局配置表';
COMMENT ON COLUMN public.ml_system_configs.config_key IS '配置唯一标识键';
COMMENT ON COLUMN public.ml_system_configs.config_value IS '配置内容 (JSONB)';
-- =====================================================================================
-- Migration: Auth 安全约束增强
-- 位置docs/sql/10_schema/auth/ak_auth_security_constraints_v1.sql
-- 对象类型ALTER TABLE / CONSTRAINT
-- 说明:增强 ak_users 与 auth.users 的关联安全性,防止孤儿数据
-- =====================================================================================

-- 1. 确保 ak_users.auth_id 存在外键约束指向 auth.users
-- 注意Supabase 的 auth.users 表在 auth schema 下,需要确保权限正确
DO $$
BEGIN
    -- 检查是否已存在外键约束
    IF NOT EXISTS (
        SELECT 1 FROM information_schema.table_constraints 
        WHERE constraint_name = 'fk_ak_users_auth_id' 
        AND table_name = 'ak_users'
    ) THEN
        -- 添加外键约束,当 auth.users 被删除时自动删除对应的 profile
        ALTER TABLE public.ak_users
        ADD CONSTRAINT fk_ak_users_auth_id
        FOREIGN KEY (auth_id) REFERENCES auth.users(id)
        ON DELETE CASCADE;
    END IF;
END $$;

-- 2. 为 auth_id 建立唯一索引,确保一个 auth 用户只有一个 profile
CREATE UNIQUE INDEX IF NOT EXISTS idx_ak_users_auth_id_unique 
ON public.ak_users(auth_id);

-- 3. 为 role 字段建立索引,加速权限查询
CREATE INDEX IF NOT EXISTS idx_ak_users_role 
ON public.ak_users(role);

-- 4. 添加检查约束,确保 role 字段只能是有效值
ALTER TABLE public.ak_users
DROP CONSTRAINT IF EXISTS chk_ak_users_role_valid;

ALTER TABLE public.ak_users
ADD CONSTRAINT chk_ak_users_role_valid
CHECK (role IN ('user', 'admin', 'staff', 'agent', 'kefu') OR role IS NULL);

-- 5. 为 ak_admin_roles 添加约束确保关联有效性
DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM information_schema.table_constraints 
        WHERE constraint_name = 'fk_ak_admin_roles_admin_id' 
        AND table_name = 'ak_admin_roles'
    ) THEN
        ALTER TABLE public.ak_admin_roles
        ADD CONSTRAINT fk_ak_admin_roles_admin_id
        FOREIGN KEY (admin_id) REFERENCES public.ak_users(id)
        ON DELETE CASCADE;
    END IF;
END $$;

-- 6. 为 ak_role_permissions 添加约束确保关联有效性
DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM information_schema.table_constraints 
        WHERE constraint_name = 'fk_ak_role_permissions_role_id' 
        AND table_name = 'ak_role_permissions'
    ) THEN
        ALTER TABLE public.ak_role_permissions
        ADD CONSTRAINT fk_ak_role_permissions_role_id
        FOREIGN KEY (role_id) REFERENCES public.ak_roles(id)
        ON DELETE CASCADE;
    END IF;
END $$;

DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM information_schema.table_constraints 
        WHERE constraint_name = 'fk_ak_role_permissions_permission_id' 
        AND table_name = 'ak_role_permissions'
    ) THEN
        ALTER TABLE public.ak_role_permissions
        ADD CONSTRAINT fk_ak_role_permissions_permission_id
        FOREIGN KEY (permission_id) REFERENCES public.ak_permissions(id)
        ON DELETE CASCADE;
    END IF;
END $$;-- =====================================================================================
-- Schema: 内容管理模块核心表
-- 位置docs/sql/10_schema/cms/ml_cms_tables_v1.sql
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:包含文章分类及文章主表定义
-- =====================================================================================

-- 1. 文章分类表
CREATE TABLE IF NOT EXISTS public.ml_article_categories (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  icon TEXT NULL,
  sort INTEGER NOT NULL DEFAULT 0,
  status SMALLINT NOT NULL DEFAULT 1, -- 1: 启用, 0: 禁用
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 唯一性约束
CREATE UNIQUE INDEX IF NOT EXISTS ml_article_categories_name_uniq ON public.ml_article_categories (name);

-- 2. 文章主表
CREATE TABLE IF NOT EXISTS public.ml_articles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  category_id UUID NOT NULL REFERENCES public.ml_article_categories(id),
  
  title TEXT NOT NULL,
  author TEXT NULL,
  image TEXT NULL,                -- 文章封面图
  description TEXT NULL,          -- 文章简介
  content TEXT NOT NULL,          -- 文章内容 (富文本)
  
  status SMALLINT NOT NULL DEFAULT 0, -- 0: 未发布, 1: 已发布
  views INTEGER NOT NULL DEFAULT 0,   -- 浏览量
  
  is_banner BOOLEAN NOT NULL DEFAULT FALSE, -- 是否展示在 banner
  is_hot BOOLEAN NOT NULL DEFAULT FALSE,    -- 是否热门
  
  linked_product_id UUID NULL,    -- 关联商品ID (可选)
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 常用查询索引
CREATE INDEX IF NOT EXISTS ml_articles_category_id_idx ON public.ml_articles (category_id);
CREATE INDEX IF NOT EXISTS ml_articles_status_idx ON public.ml_articles (status);
CREATE INDEX IF NOT EXISTS ml_articles_created_at_idx ON public.ml_articles (created_at DESC);
-- =====================================================================================
-- Schema: 装修模块 - DIY 页面配置表
-- 位置docs/sql/10_schema/decoration/ak_diy_pages_v1.sql
-- 对象类型TABLE
-- 版本v1
-- 说明:存储首页、专题页及个人中心的 DIY 布局 JSON 配置
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_diy_pages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    type TEXT NOT NULL,             -- home: 首页, topic: 专题页, user: 个人中心
    
    config JSONB NOT NULL DEFAULT '{}'::jsonb, -- 核心布局配置 (组件列表及参数)
    
    is_home BOOLEAN NOT NULL DEFAULT FALSE,    -- 是否为生效首页
    is_active BOOLEAN NOT NULL DEFAULT TRUE,   -- 是否启用
    
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    created_by UUID REFERENCES public.ak_users(id),
    updated_by UUID REFERENCES public.ak_users(id)
);

-- 索引
CREATE INDEX IF NOT EXISTS idx_diy_pages_type ON public.ak_diy_pages (type);
CREATE INDEX IF NOT EXISTS idx_diy_pages_is_home ON public.ak_diy_pages (is_home) WHERE is_home = TRUE;

-- 注释
COMMENT ON TABLE public.ak_diy_pages IS 'DIY 页面装修配置表';
COMMENT ON COLUMN public.ak_diy_pages.type IS '页面类型: home(首页), topic(专题), user(个人中心)';
COMMENT ON COLUMN public.ak_diy_pages.config IS 'DIY 布局配置 JSON';
-- =====================================================================================
-- Schema: 物流设置 (Delivery) 核心表
-- 位置docs/sql/10_schema/delivery/ak_delivery_system_v1.sql
-- 对象类型TABLE
-- 版本v1
-- 说明:包含配送员管理表、提货点/核销点管理表
-- =====================================================================================

-- 1. 配送员管理表
CREATE TABLE IF NOT EXISTS public.ml_delivery_staff (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    uid UUID REFERENCES public.ak_users(id) ON DELETE SET NULL, -- 关联用户(可选)
    
    nickname TEXT NOT NULL,         -- 配送员名称
    avatar TEXT,                    -- 头像
    phone TEXT NOT NULL,            -- 手机号
    
    status SMALLINT NOT NULL DEFAULT 1, -- 状态: 1-启用, 0-禁用
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 2. 提货点/核销点管理表
CREATE TABLE IF NOT EXISTS public.ml_delivery_stations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,             -- 提货点名称
    phone TEXT NOT NULL,            -- 联系电话
    address TEXT NOT NULL,          -- 详细地址
    
    image TEXT,                     -- 门店图片
    lng NUMERIC(10,7),              -- 经度
    lat NUMERIC(10,7),              -- 纬度
    
    status SMALLINT NOT NULL DEFAULT 1, -- 状态: 1-显示, 0-隐藏
    sort_order INTEGER DEFAULT 0,
    
    business_hours JSONB,           -- 营业时间 (如: {"start": "09:00", "end": "21:00"})
    
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 索引
CREATE INDEX IF NOT EXISTS idx_delivery_staff_phone ON public.ml_delivery_staff(phone);
CREATE INDEX IF NOT EXISTS idx_delivery_stations_status ON public.ml_delivery_stations(status);

-- 注释
COMMENT ON TABLE public.ml_delivery_staff IS '配送员信息表';
COMMENT ON TABLE public.ml_delivery_stations IS '提货点/核销点信息表';
-- =====================================================================================
-- Schema: 医养执行端 Delivery 基础表升级
-- 位置docs/sql/10_schema/delivery/ak_delivery_system_v2.sql
-- 对象类型ALTER TABLE / INDEX / TRIGGER
-- 版本v2
-- 说明:在 v1 基础上最小升级为“医养上门服务执行端”可用模型。
--       补齐 uid 唯一索引、软删除、机构关联、staff_no、在线状态、资质状态。
-- =====================================================================================

-- 0. 通用 updated_at 触发器
CREATE OR REPLACE FUNCTION public.tg_set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$;

-- 1. 配送/执行人员表升级
ALTER TABLE public.ml_delivery_staff
  ADD COLUMN IF NOT EXISTS station_id UUID REFERENCES public.ml_delivery_stations(id) ON DELETE SET NULL,
  ADD COLUMN IF NOT EXISTS staff_no TEXT,
  ADD COLUMN IF NOT EXISTS online_status TEXT NOT NULL DEFAULT 'resting',
  ADD COLUMN IF NOT EXISTS certificate_status TEXT NOT NULL DEFAULT 'pending',
  ADD COLUMN IF NOT EXISTS certificate_expire_at DATE,
  ADD COLUMN IF NOT EXISTS service_area TEXT NOT NULL DEFAULT '',
  ADD COLUMN IF NOT EXISTS skills JSONB NOT NULL DEFAULT '[]'::jsonb,
  ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ,
  ADD COLUMN IF NOT EXISTS deleted_by UUID REFERENCES public.ak_users(id) ON DELETE SET NULL;

ALTER TABLE public.ml_delivery_staff
  DROP CONSTRAINT IF EXISTS chk_ml_delivery_staff_online_status;
ALTER TABLE public.ml_delivery_staff
  ADD CONSTRAINT chk_ml_delivery_staff_online_status
  CHECK (online_status IN ('online', 'resting', 'busy'));

ALTER TABLE public.ml_delivery_staff
  DROP CONSTRAINT IF EXISTS chk_ml_delivery_staff_certificate_status;
ALTER TABLE public.ml_delivery_staff
  ADD CONSTRAINT chk_ml_delivery_staff_certificate_status
  CHECK (certificate_status IN ('valid', 'expired', 'pending'));

-- 2. 站点/机构表升级
ALTER TABLE public.ml_delivery_stations
  ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ,
  ADD COLUMN IF NOT EXISTS deleted_by UUID REFERENCES public.ak_users(id) ON DELETE SET NULL;

-- 3. 索引
CREATE UNIQUE INDEX IF NOT EXISTS uq_ml_delivery_staff_uid_active
  ON public.ml_delivery_staff(uid)
  WHERE uid IS NOT NULL AND deleted_at IS NULL;

CREATE UNIQUE INDEX IF NOT EXISTS uq_ml_delivery_staff_staff_no_active
  ON public.ml_delivery_staff(staff_no)
  WHERE staff_no IS NOT NULL AND deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS idx_ml_delivery_staff_station_id
  ON public.ml_delivery_staff(station_id)
  WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS idx_ml_delivery_staff_deleted_at
  ON public.ml_delivery_staff(deleted_at);

CREATE INDEX IF NOT EXISTS idx_ml_delivery_staff_status_active
  ON public.ml_delivery_staff(status, is_active)
  WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS idx_ml_delivery_stations_deleted_at
  ON public.ml_delivery_stations(deleted_at);

CREATE INDEX IF NOT EXISTS idx_ml_delivery_stations_status_active
  ON public.ml_delivery_stations(status)
  WHERE deleted_at IS NULL;

-- 4. 自动维护 updated_at
DROP TRIGGER IF EXISTS trg_ml_delivery_staff_set_updated_at ON public.ml_delivery_staff;
CREATE TRIGGER trg_ml_delivery_staff_set_updated_at
BEFORE UPDATE ON public.ml_delivery_staff
FOR EACH ROW
EXECUTE FUNCTION public.tg_set_updated_at();

DROP TRIGGER IF EXISTS trg_ml_delivery_stations_set_updated_at ON public.ml_delivery_stations;
CREATE TRIGGER trg_ml_delivery_stations_set_updated_at
BEFORE UPDATE ON public.ml_delivery_stations
FOR EACH ROW
EXECUTE FUNCTION public.tg_set_updated_at();

-- 5. 注释
COMMENT ON COLUMN public.ml_delivery_staff.station_id IS '所属机构/服务站点 ID';
COMMENT ON COLUMN public.ml_delivery_staff.staff_no IS '服务人员编号';
COMMENT ON COLUMN public.ml_delivery_staff.online_status IS '在线状态online/resting/busy';
COMMENT ON COLUMN public.ml_delivery_staff.certificate_status IS '资质状态valid/expired/pending';
COMMENT ON COLUMN public.ml_delivery_staff.certificate_expire_at IS '主要资质到期日期';
COMMENT ON COLUMN public.ml_delivery_staff.service_area IS '服务区域描述';
COMMENT ON COLUMN public.ml_delivery_staff.skills IS '技能标签 JSON 数组';
COMMENT ON COLUMN public.ml_delivery_staff.deleted_at IS '软删除时间';
COMMENT ON COLUMN public.ml_delivery_staff.deleted_by IS '软删除操作人';
COMMENT ON COLUMN public.ml_delivery_stations.deleted_at IS '软删除时间';
COMMENT ON COLUMN public.ml_delivery_stations.deleted_by IS '软删除操作人';
-- 佣金流水表:记录每一笔佣金变动(冻结/可用/已提现/取消)
CREATE TABLE IF NOT EXISTS public.ak_commission_logs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- 佣金获得者
  uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
  -- 佣金来源用户(下单用户/被推广用户,可为空)
  source_uid UUID REFERENCES public.ak_users(id) ON DELETE SET NULL,

  -- 关联订单
  order_id UUID REFERENCES public.ml_orders(id) ON DELETE SET NULL,
  order_no VARCHAR(50),

  -- 金额与状态
  amount DECIMAL(12,2) NOT NULL DEFAULT 0,
  status TEXT NOT NULL DEFAULT 'frozen', -- frozen/available/withdrawn/canceled

  -- 冻结到期时间(用于解冻逻辑)
  frozen_until TIMESTAMPTZ,

  remark TEXT,

  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now(),

  CONSTRAINT chk_ak_commission_amount_nonneg CHECK (amount >= 0),
  CONSTRAINT chk_ak_commission_status CHECK (status IN ('frozen','available','withdrawn','canceled'))
);

CREATE INDEX IF NOT EXISTS idx_ak_commission_logs_uid ON public.ak_commission_logs(uid);
CREATE INDEX IF NOT EXISTS idx_ak_commission_logs_order_id ON public.ak_commission_logs(order_id);
CREATE INDEX IF NOT EXISTS idx_ak_commission_logs_status ON public.ak_commission_logs(status);
CREATE INDEX IF NOT EXISTS idx_ak_commission_logs_created_at ON public.ak_commission_logs(created_at);

-- 启用 RLS
ALTER TABLE public.ak_commission_logs ENABLE ROW LEVEL SECURITY;

-- Admin 可读写
CREATE POLICY "Admins can manage commission logs"
ON public.ak_commission_logs
FOR ALL
TO authenticated
USING (
  EXISTS (
    SELECT 1 FROM public.ak_users
    WHERE id = auth.uid() AND role = 'admin'
  )
)
WITH CHECK (
  EXISTS (
    SELECT 1 FROM public.ak_users
    WHERE id = auth.uid() AND role = 'admin'
  )
);

-- 用户仅可查看自己的佣金流水
CREATE POLICY "Users can view own commission logs"
ON public.ak_commission_logs
FOR SELECT
TO authenticated
USING (uid = auth.uid());
-- =====================================================================================
-- Schema: 分销代理商申请表
-- 位置docs/sql/10_schema/distribution/ak_distribution_agent_applications_v1.sql
-- 对象类型TABLE
-- 版本v1
-- 依赖ak_users, ak_distribution_divisions
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_distribution_agent_applications (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    division_uid UUID NOT NULL REFERENCES public.ak_distribution_divisions(uid),

    agent_name TEXT NOT NULL,
    agent_phone TEXT NULL,

    proof_images JSONB NULL, -- 申请凭证图片列表

    status TEXT NOT NULL DEFAULT 'pending', -- pending/approved/rejected
    refusal_reason TEXT NULL,

    approved_at TIMESTAMPTZ NULL,
    approved_by UUID NULL REFERENCES public.ak_users(id),

    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_dist_agent_applications_uid ON public.ak_distribution_agent_applications(uid);
CREATE INDEX IF NOT EXISTS idx_dist_agent_applications_division_uid ON public.ak_distribution_agent_applications(division_uid);
CREATE INDEX IF NOT EXISTS idx_dist_agent_applications_status ON public.ak_distribution_agent_applications(status);

COMMENT ON TABLE public.ak_distribution_agent_applications IS '分销代理商申请记录表';
COMMENT ON COLUMN public.ak_distribution_agent_applications.proof_images IS '申请图片列表(JSON)';
-- =====================================================================================
-- Schema: 分销代理商管理表
-- 位置docs/sql/10_schema/distribution/ak_distribution_agents_v1.sql
-- 对象类型TABLE
-- 版本v1
-- 依赖ak_users, ak_distribution_divisions
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_distribution_agents (
    uid UUID PRIMARY KEY REFERENCES public.ak_users(id) ON DELETE CASCADE,
    division_uid UUID NOT NULL REFERENCES public.ak_distribution_divisions(uid), -- 所属事业部
    name TEXT NOT NULL,
    commission_ratio NUMERIC(5,2) DEFAULT 0 CHECK (commission_ratio >= 0 AND commission_ratio <= 100),
    is_enabled BOOLEAN DEFAULT TRUE,
    end_time TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now(),
    created_by UUID REFERENCES public.ak_users(id),
    updated_by UUID REFERENCES public.ak_users(id)
);

-- 索引
CREATE INDEX IF NOT EXISTS idx_distribution_agents_division_uid ON public.ak_distribution_agents(division_uid);

-- 注释
COMMENT ON TABLE public.ak_distribution_agents IS '分销代理商信息表';
COMMENT ON COLUMN public.ak_distribution_agents.uid IS '用户ID关联代理商本人';
COMMENT ON COLUMN public.ak_distribution_agents.division_uid IS '所属事业部UID';
COMMENT ON COLUMN public.ak_distribution_agents.commission_ratio IS '代理商固定分佣比例(%)';
-- 1. 创建分销配置表
CREATE TABLE IF NOT EXISTS public.ak_distribution_config (
    id TEXT PRIMARY KEY DEFAULT 'global_config',
    is_enabled BOOLEAN DEFAULT true,               -- 分销启用
    extract_type TEXT DEFAULT '2',                  -- 分销模式: 1指定, 2人人, 3满额
    bind_type TEXT DEFAULT '2',                     -- 绑定关系: 1所有用户, 2新用户
    store_brokerage_binding_status TEXT DEFAULT '1', -- 绑定模式: 1永久, 2有效期, 3临时
    brokerage_poster_status TEXT,                   -- 分销海报图路径/URL
    brokerage_level INTEGER DEFAULT 2,              -- 分销层级: 1, 2
    is_area_manager BOOLEAN DEFAULT true,           -- 事业部开关
    is_agent_apply BOOLEAN DEFAULT true,            -- 代理商申请开关
    is_commission_window BOOLEAN DEFAULT true,      -- 佣金悬浮窗开关
    
    -- 返佣设置
    is_self_brokerage BOOLEAN DEFAULT true,         -- 自购返佣
    is_member_brokerage BOOLEAN DEFAULT false,      -- 购买会员返佣
    brokerage_type TEXT DEFAULT '1',                -- 返佣类型: 1价格, 2实付
    is_promoter_brokerage BOOLEAN DEFAULT true,     -- 推广用户返佣
    promoter_brokerage_price DECIMAL(10,2) DEFAULT 2.00,
    promoter_brokerage_day_max DECIMAL(10,2) DEFAULT -1.00,
    store_brokerage_ratio DECIMAL(10,2) DEFAULT 20.00,
    store_brokerage_two_ratio DECIMAL(10,2) DEFAULT 2.00,
    extract_frozen_time INTEGER DEFAULT 1,
    
    -- 提现设置
    user_extract_min_price DECIMAL(10,2) DEFAULT 1.00,
    extract_bank_list TEXT DEFAULT '中国银行',
    extract_type_list TEXT[] DEFAULT ARRAY['bank', 'wechat', 'alipay'],
    wechat_extract_type TEXT DEFAULT '1',
    alipay_extract_type TEXT DEFAULT '1',
    user_extract_fee DECIMAL(10,2) DEFAULT 0.00,
    
    updated_at TIMESTAMPTZ DEFAULT now(),
    updated_by UUID REFERENCES auth.users(id)
);

-- 2. 启用 RLS
ALTER TABLE public.ak_distribution_config ENABLE ROW LEVEL SECURITY;

-- 3. 创建权限策略 (基于公共角色函数或直接查询 ak_users)
-- 允许 Admin 查看配置
CREATE POLICY "Admins can view distribution config" 
ON public.ak_distribution_config FOR SELECT 
TO authenticated
USING (
  EXISTS (
    SELECT 1 FROM public.ak_users 
    WHERE auth_id = auth.uid() AND role = 'admin'
  )
);

-- 允许 Admin 修改配置
CREATE POLICY "Admins can update distribution config" 
ON public.ak_distribution_config FOR ALL 
TO authenticated
USING (
  EXISTS (
    SELECT 1 FROM public.ak_users 
    WHERE auth_id = auth.uid() AND role = 'admin'
  )
)
WITH CHECK (
  EXISTS (
    SELECT 1 FROM public.ak_users 
    WHERE auth_id = auth.uid() AND role = 'admin'
  )
);

-- 4. 插入初始化数据
INSERT INTO public.ak_distribution_config (id) 
VALUES ('global_config')
ON CONFLICT (id) DO NOTHING;
-- =====================================================================================
-- Schema: 分销事业部申请表
-- 位置docs/sql/10_schema/distribution/ak_distribution_division_applications_v1.sql
-- 说明:记录用户申请加入事业部成为代理商的流水,支持审核流转,按商家隔离。
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_distribution_division_applications (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    division_id UUID NOT NULL REFERENCES public.ak_distribution_divisions(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL,                          -- 申请人填写的代理商名称
    phone TEXT NOT NULL,                         -- 申请人联系电话
    images JSONB DEFAULT '[]'::jsonb,            -- 申请附件图片 (数组)
    
    status INTEGER DEFAULT 1,                    -- 状态: 1待审核, 2已同意, 3已拒绝
    admin_remark TEXT,                           -- 审核备注
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 启用 RLS
ALTER TABLE public.ak_distribution_division_applications ENABLE ROW LEVEL SECURITY;

-- 权限策略
CREATE POLICY "Merchants manage their own applications" 
ON public.ak_distribution_division_applications FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许用户提交和查看自己的申请
CREATE POLICY "Users handle own applications" 
ON public.ak_distribution_division_applications FOR ALL 
TO authenticated
USING (uid = auth.uid())
WITH CHECK (uid = auth.uid());

-- 索引
CREATE INDEX IF NOT EXISTS idx_div_app_merchant ON public.ak_distribution_division_applications(merchant_id);
CREATE INDEX IF NOT EXISTS idx_div_app_uid ON public.ak_distribution_division_applications(uid);
CREATE INDEX IF NOT EXISTS idx_div_app_status ON public.ak_distribution_division_applications(status);
-- =====================================================================================
-- Schema: 分销事业部管理表
-- 位置docs/sql/10_schema/distribution/ak_distribution_divisions_v1.sql
-- 对象类型TABLE
-- 版本v1
-- 依赖ak_users
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_distribution_divisions (
    uid UUID PRIMARY KEY REFERENCES public.ak_users(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    invite_code TEXT UNIQUE NOT NULL,
    commission_ratio NUMERIC(5,2) DEFAULT 0 CHECK (commission_ratio >= 0 AND commission_ratio <= 100),
    is_enabled BOOLEAN DEFAULT TRUE,
    end_time TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now(),
    created_by UUID REFERENCES public.ak_users(id),
    updated_by UUID REFERENCES public.ak_users(id)
);

-- 索引
CREATE INDEX IF NOT EXISTS idx_distribution_divisions_invite_code ON public.ak_distribution_divisions(invite_code);

-- 注释
COMMENT ON TABLE public.ak_distribution_divisions IS '分销事业部信息表';
COMMENT ON COLUMN public.ak_distribution_divisions.uid IS '用户ID关联事业部负责人';
COMMENT ON COLUMN public.ak_distribution_divisions.invite_code IS '事业部专属邀请码';
COMMENT ON COLUMN public.ak_distribution_divisions.commission_ratio IS '事业部固定分佣比例(%)';
COMMENT ON COLUMN public.ak_distribution_divisions.end_time IS '事业部有效截止时间';
-- 1. 创建分销等级表
CREATE TABLE IF NOT EXISTS public.ak_distribution_level (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,                         -- 等级名称
    level INTEGER NOT NULL UNIQUE,              -- 等级权重/数字如1, 2, 3
    percent1 DECIMAL(10,2) DEFAULT 0,           -- 一级分佣比例 (%)
    percent2 DECIMAL(10,2) DEFAULT 0,           -- 二级分佣比例 (%)
    task_total INTEGER DEFAULT 0,               -- 任务总数
    task_finish INTEGER DEFAULT 0,              -- 需完成数量(升级门槛)
    is_visible BOOLEAN DEFAULT true,            -- 是否显示
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 2. 启用 RLS
ALTER TABLE public.ak_distribution_level ENABLE ROW LEVEL SECURITY;

-- 3. 创建权限策略
-- 允许所有认证用户查看等级(用于前端展示)
CREATE POLICY "Anyone can view levels" 
ON public.ak_distribution_level FOR SELECT 
TO authenticated
USING (true);

-- 仅允许 Admin 进行管理 (INSERT/UPDATE/DELETE)
CREATE POLICY "Admins can manage levels" 
ON public.ak_distribution_level FOR ALL 
TO authenticated
USING (
  EXISTS (
    SELECT 1 FROM public.ak_users 
    WHERE auth_id = auth.uid() AND role = 'admin'
  )
)
WITH CHECK (
  EXISTS (
    SELECT 1 FROM public.ak_users 
    WHERE auth_id = auth.uid() AND role = 'admin'
  )
);

-- 4. 插入初始化示例数据
INSERT INTO public.ak_distribution_level (name, level, percent1, percent2, is_visible) 
VALUES ('普通分销员', 1, 10.00, 5.00, true)
ON CONFLICT (level) DO NOTHING;
-- 推广员关系表:记录下级与上级(邀请人)的绑定关系
CREATE TABLE IF NOT EXISTS public.ak_promoter_relations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
  inviter_uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
  bind_time TIMESTAMPTZ DEFAULT now(),
  created_at TIMESTAMPTZ DEFAULT now(),

  CONSTRAINT chk_ak_promoter_relations_no_self CHECK (uid <> inviter_uid),
  CONSTRAINT uq_ak_promoter_relations_uid UNIQUE (uid)
);

CREATE INDEX IF NOT EXISTS idx_ak_promoter_relations_inviter_uid ON public.ak_promoter_relations(inviter_uid);

-- 启用 RLS
ALTER TABLE public.ak_promoter_relations ENABLE ROW LEVEL SECURITY;

-- Admin 可读写
CREATE POLICY "Admins can manage promoter relations"
ON public.ak_promoter_relations
FOR ALL
TO authenticated
USING (
  EXISTS (
    SELECT 1 FROM public.ak_users
    WHERE id = auth.uid() AND role = 'admin'
  )
)
WITH CHECK (
  EXISTS (
    SELECT 1 FROM public.ak_users
    WHERE id = auth.uid() AND role = 'admin'
  )
);

-- 普通用户可查看与自己相关的关系(可选,便于移动端展示上级/下级)
CREATE POLICY "Users can view their promoter relation"
ON public.ak_promoter_relations
FOR SELECT
TO authenticated
USING (uid = auth.uid() OR inviter_uid = auth.uid());
-- =====================================================================================
-- Schema: 用户提现申请表
-- 位置docs/sql/10_schema/finance/
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:管理用户发起的提现申请(佣金/余额),支持多种提现方式及快照信息
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ml_extract (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  uid UUID NOT NULL REFERENCES public.ak_users(id),
  
  real_name TEXT NULL,          -- 提现人姓名快照
  extract_type TEXT NOT NULL,   -- 提现方式: alipay, wechat, bank
  
  -- 账号快照信息
  alipay_code TEXT NULL,        -- 支付宝账号
  wechat_code TEXT NULL,        -- 微信账号
  bank_code TEXT NULL,          -- 银行卡号
  bank_address TEXT NULL,       -- 开户行地址
  
  extract_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 申请提现金额
  service_fee DECIMAL(12,2) NOT NULL DEFAULT 0,   -- 提现手续费
  balance DECIMAL(12,2) NOT NULL DEFAULT 0,       -- 提现时的余额快照
  
  status SMALLINT NOT NULL DEFAULT 0, -- 状态: 0:待审核, 1:已通过, -1:已驳回
  refusal_reason TEXT NULL,           -- 驳回原因
  
  admin_id UUID NULL REFERENCES public.ak_users(id), -- 审核人ID
  payment_time TIMESTAMPTZ NULL,      -- 打款/到账时间
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 索引
CREATE INDEX IF NOT EXISTS ml_extract_uid_idx ON public.ml_extract (uid);
CREATE INDEX IF NOT EXISTS ml_extract_status_idx ON public.ml_extract (status);
CREATE INDEX IF NOT EXISTS ml_extract_created_at_idx ON public.ml_extract (created_at DESC);
-- =====================================================================================
-- Schema: 发票管理表
-- 位置docs/sql/10_schema/finance/ml_invoices_v1.sql
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:记录用户提交的开票申请及其处理状态
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ml_invoices (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  uid UUID NOT NULL REFERENCES public.ak_users(id),
  order_no TEXT NOT NULL,           -- 关联订单号
  
  order_amount DECIMAL(12,2) NOT NULL, -- 订单金额
  
  invoice_type SMALLINT NOT NULL DEFAULT 1, -- 1: 电子普通发票, 2: 增值税专用发票
  header_type SMALLINT NOT NULL DEFAULT 1,  -- 1: 个人, 2: 企业
  
  header_name TEXT NOT NULL,        -- 发票抬头
  tax_id TEXT NULL,                 -- 企业税号
  
  email TEXT NULL,                  -- 接收邮箱
  remark TEXT NULL,                 -- 备注
  
  status SMALLINT NOT NULL DEFAULT 0, -- 0: 待开票, 1: 已开票, -1: 已拒绝
  refusal_reason TEXT NULL,           -- 驳回原因
  invoice_url TEXT NULL,              -- 电子发票文件路径/URL
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 索引
CREATE INDEX IF NOT EXISTS ml_invoices_uid_idx ON public.ml_invoices (uid);
CREATE INDEX IF NOT EXISTS ml_invoices_order_no_idx ON public.ml_invoices (order_no);
CREATE INDEX IF NOT EXISTS ml_invoices_status_idx ON public.ml_invoices (status);
-- =====================================================================================
-- Schema: 用户资金流水表
-- 位置docs/sql/10_schema/finance/
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:记录用户余额、积分、佣金的所有增减流水(原子日志)
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ml_user_bill (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  uid UUID NOT NULL REFERENCES public.ak_users(id),
  
  link_id TEXT NULL,           -- 关联业务ID订单号、提现ID、充值ID等
  pm SMALLINT NOT NULL DEFAULT 1, -- 0:支出, 1:收入
  
  title TEXT NOT NULL,         -- 流水标题(如:商品购买、充值、提现)
  category TEXT NOT NULL,      -- 业务大类balance-余额, integral-积分, brokerage-佣金)
  type TEXT NOT NULL,          -- 业务子类型recharge, extract, pay, refund, system_add, system_sub
  
  number DECIMAL(12,2) NOT NULL DEFAULT 0, -- 变动金额
  balance DECIMAL(12,2) NOT NULL DEFAULT 0, -- 变动后的余额快照
  
  mark TEXT NULL,              -- 备注
  status SMALLINT NOT NULL DEFAULT 1, -- 状态1:有效, 0:无效/冲正)
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 常用查询索引
CREATE INDEX IF NOT EXISTS ml_user_bill_uid_idx ON public.ml_user_bill (uid);
CREATE INDEX IF NOT EXISTS ml_user_bill_category_type_idx ON public.ml_user_bill (category, type);
CREATE INDEX IF NOT EXISTS ml_user_bill_created_at_idx ON public.ml_user_bill (created_at DESC);
-- =====================================================================================
-- Schema: 用户充值记录表
-- 位置docs/sql/10_schema/finance/
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:记录用户主动发起的充值申请及支付状态
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ml_user_recharge (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  uid UUID NOT NULL REFERENCES public.ak_users(id),
  
  order_no TEXT UNIQUE NOT NULL,    -- 充值订单号cz开头
  recharge_type TEXT NOT NULL,      -- 充值渠道: wechat, alipay, system (后台补单)
  
  price DECIMAL(12,2) NOT NULL DEFAULT 0,      -- 实际充值金额
  give_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 赠送金额
  
  paid SMALLINT NOT NULL DEFAULT 0, -- 支付状态: 0:未支付, 1:已支付
  pay_time TIMESTAMPTZ NULL,        -- 支付时间
  
  channel_trade_no TEXT NULL,       -- 外部渠道流水号
  status SMALLINT NOT NULL DEFAULT 1, -- 记录状态: 1:正常, 0:逻辑删除
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 索引
CREATE INDEX IF NOT EXISTS ml_user_recharge_uid_idx ON public.ml_user_recharge (uid);
CREATE INDEX IF NOT EXISTS ml_user_recharge_order_no_idx ON public.ml_user_recharge (order_no);
CREATE INDEX IF NOT EXISTS ml_user_recharge_created_at_idx ON public.ml_user_recharge (created_at DESC);
-- =====================================================================================
-- Schema: homecare foundation
-- Version: v1
-- Purpose: 为 ec_service_requests / ec_care_tasks / ec_care_records / hc_* 新链补齐最小结构、索引与基础 RLS。
-- Coverage:
-- 1. consumer 下单与自动派单字段
-- 2. consumer 验收 / 退回整改 / 评价字段
-- 3. delivery 执行记录、异常、证据、事件字段
-- =====================================================================================

-- 执行稿见mall_sql/migrations/20260526_homecare_foundation_v1.sql-- =====================================================================================
-- Schema: 客服模块核心表
-- 位置docs/sql/10_schema/kefu/ml_kefu_tables_v1.sql
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:包含客服账号、话术、留言及自动回复逻辑
-- =====================================================================================

-- 1. 客服人员表
CREATE TABLE IF NOT EXISTS public.ml_kefu_accounts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES public.ak_users(id), -- 关联主用户表
  
  nickname TEXT NOT NULL,         -- 客服昵称
  avatar TEXT NULL,               -- 客服头像
  
  status SMALLINT NOT NULL DEFAULT 1, -- 1:启用, 0:禁用
  is_online BOOLEAN NOT NULL DEFAULT FALSE,
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 2. 话术分类表
CREATE TABLE IF NOT EXISTS public.ml_kefu_word_categories (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  sort INT NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 3. 客服快捷话术表
CREATE TABLE IF NOT EXISTS public.ml_kefu_words (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  category_id UUID NOT NULL REFERENCES public.ml_kefu_word_categories(id) ON DELETE CASCADE,
  
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  sort INT NOT NULL DEFAULT 0,
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 4. 用户留言反馈表
CREATE TABLE IF NOT EXISTS public.ml_kefu_feedbacks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NULL REFERENCES public.ak_users(id), -- 允许匿名留言
  
  nickname TEXT NULL,
  phone TEXT NULL,
  content TEXT NOT NULL,
  
  status SMALLINT NOT NULL DEFAULT 0, -- 0:未处理, 1:已处理
  reply_content TEXT NULL,            -- 管理员回复内容
  processed_at TIMESTAMPTZ NULL,      -- 处理时间
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 5. 关键词自动回复表
CREATE TABLE IF NOT EXISTS public.ml_kefu_auto_replies (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  keyword TEXT NOT NULL,
  content TEXT NOT NULL,
  reply_type TEXT NOT NULL DEFAULT 'text', -- text, image
  
  status SMALLINT NOT NULL DEFAULT 1, -- 1:开启, 0:关闭
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 索引
CREATE INDEX IF NOT EXISTS ml_kefu_accounts_user_id_idx ON public.ml_kefu_accounts (user_id);
CREATE INDEX IF NOT EXISTS ml_kefu_words_category_id_idx ON public.ml_kefu_words (category_id);
CREATE INDEX IF NOT EXISTS ml_kefu_feedbacks_status_idx ON public.ml_kefu_feedbacks (status);
CREATE INDEX IF NOT EXISTS ml_kefu_auto_replies_keyword_idx ON public.ml_kefu_auto_replies (keyword);
-- =====================================================================================
-- Schema: 秒杀与拼团活动表
-- 位置docs/sql/10_schema/marketing/ak_advanced_marketing_v1.sql
-- 说明:管理秒杀活动与拼团活动,按商家隔离。
-- =====================================================================================

-- 1. 秒杀活动表
CREATE TABLE IF NOT EXISTS public.ak_seckill_activities (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    title TEXT NOT NULL,                         -- 活动标题
    single_limit INTEGER DEFAULT 1,              -- 单次限购
    total_limit INTEGER DEFAULT 10,              -- 总购买数量限制
    product_count INTEGER DEFAULT 0,             -- 包含商品数量
    time_range TEXT NOT NULL,                    -- 活动时段 (如 "06:00-24:00")
    
    start_date TIMESTAMPTZ NOT NULL,             -- 开始日期
    end_date TIMESTAMPTZ NOT NULL,               -- 结束日期
    
    status BOOLEAN DEFAULT true,                 -- 状态: true开启, false关闭
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 2. 拼团活动表 (开团记录)
CREATE TABLE IF NOT EXISTS public.ak_combination_activities (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    uid UUID NOT NULL REFERENCES public.ak_users(id), -- 开团团长
    product_id UUID NOT NULL REFERENCES public.ml_products(id), -- 拼团商品
    
    people INTEGER DEFAULT 2,                    -- 几人团
    count_people INTEGER DEFAULT 1,              -- 当前几人参加
    
    start_time TIMESTAMPTZ DEFAULT now(),        -- 开团时间
    stop_time TIMESTAMPTZ NOT NULL,              -- 结束时间
    
    status TEXT NOT NULL DEFAULT 'ongoing',      -- ongoing进行中, pending未完成, ended已成功
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now(),

    CONSTRAINT chk_comb_status CHECK (status IN ('ongoing', 'pending', 'ended'))
);

-- 3. 启用 RLS
ALTER TABLE public.ak_seckill_activities ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_combination_activities ENABLE ROW LEVEL SECURITY;

-- 4. 创建权限策略 (按 merchant_id 隔离)
-- 秒杀策略
CREATE POLICY "Merchants can manage their own seckill activities"
ON public.ak_seckill_activities FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 拼团策略
CREATE POLICY "Merchants can manage their own combination activities"
ON public.ak_combination_activities FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许所有认证用户查看(用于移动端展示)
CREATE POLICY "Anyone can view active marketing activities"
ON public.ak_seckill_activities FOR SELECT 
TO authenticated
USING (status = true);

CREATE POLICY "Anyone can view ongoing combinations"
ON public.ak_combination_activities FOR SELECT 
TO authenticated
USING (true);
-- =====================================================================================
-- Schema: 砍价与团购活动表
-- 位置docs/sql/10_schema/marketing/ak_bargain_groupbuy_v1.sql
-- 说明:管理砍价与团购活动,按商家隔离。
-- =====================================================================================

-- 1. 砍价活动表
CREATE TABLE IF NOT EXISTS public.ak_marketing_bargains (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_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,
    
    title TEXT NOT NULL,                         -- 活动标题
    min_price DECIMAL(12,2) NOT NULL DEFAULT 0,  -- 砍价最低价
    stock INTEGER DEFAULT 0,                     -- 活动库存
    
    start_time TIMESTAMPTZ NOT NULL,             -- 开始时间
    stop_time TIMESTAMPTZ NOT NULL,              -- 结束时间
    
    status BOOLEAN DEFAULT true,                 -- 状态: true开启, false关闭
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 2. 团购活动表
CREATE TABLE IF NOT EXISTS public.ak_marketing_groupbuys (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_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,
    
    title TEXT NOT NULL,                         -- 活动标题
    price DECIMAL(12,2) NOT NULL DEFAULT 0,      -- 团购价格
    people INTEGER DEFAULT 2,                    -- 成团人数要求
    stock INTEGER DEFAULT 0,                     -- 活动库存
    
    start_time TIMESTAMPTZ NOT NULL,             -- 开始时间
    stop_time TIMESTAMPTZ NOT NULL,              -- 结束时间
    
    status BOOLEAN DEFAULT true,                 -- 状态: true开启, false关闭
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 3. 启用 RLS
ALTER TABLE public.ak_marketing_bargains ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_groupbuys ENABLE ROW LEVEL SECURITY;

-- 4. 创建权限策略 (按 merchant_id 隔离)
-- 砍价策略
CREATE POLICY "Merchants can manage their own bargains"
ON public.ak_marketing_bargains FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 团购策略
CREATE POLICY "Merchants can manage their own groupbuys"
ON public.ak_marketing_groupbuys FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许所有认证用户查看(用于移动端展示)
CREATE POLICY "Anyone can view active marketing activities"
ON public.ak_marketing_bargains FOR SELECT 
TO authenticated
USING (status = true);

CREATE POLICY "Anyone can view active groupbuys"
ON public.ak_marketing_groupbuys FOR SELECT 
TO authenticated
USING (status = true);

-- 5. 索引
CREATE INDEX IF NOT EXISTS idx_bargains_merchant ON public.ak_marketing_bargains(merchant_id);
CREATE INDEX IF NOT EXISTS idx_groupbuys_merchant ON public.ak_marketing_groupbuys(merchant_id);
-- =====================================================================================
-- Schema: 直播商品管理表
-- 位置docs/sql/10_schema/marketing/ak_live_products_v1.sql
-- 说明:管理直播活动关联的商品,支持直播价设置与审核状态,按商家隔离。
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_marketing_live_products (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_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,
    
    live_price DECIMAL(12,2) NOT NULL DEFAULT 0,  -- 直播专属价
    stock INTEGER DEFAULT 0,                     -- 直播可用库存
    
    audit_status INTEGER DEFAULT 1,              -- 审核状态: 1待审核, 2审核通过, 3审核驳回
    is_show BOOLEAN DEFAULT true,                -- 是否在直播间显示
    
    sort_order INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 启用 RLS
ALTER TABLE public.ak_marketing_live_products ENABLE ROW LEVEL SECURITY;

-- 权限策略:商家仅能管理自己的直播商品
CREATE POLICY "Merchants can manage their own live products"
ON public.ak_marketing_live_products FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许查看审核通过的商品
CREATE POLICY "Anyone can view approved live products"
ON public.ak_marketing_live_products FOR SELECT 
TO authenticated
USING (audit_status = 2 AND is_show = true);

-- 索引
CREATE INDEX IF NOT EXISTS idx_live_products_merchant ON public.ak_marketing_live_products(merchant_id);
CREATE INDEX IF NOT EXISTS idx_live_products_product ON public.ak_marketing_live_products(product_id);
-- =====================================================================================
-- Schema: 抽奖与直播管理表
-- 位置docs/sql/10_schema/marketing/ak_lottery_live_v1.sql
-- 说明:管理抽奖活动、奖品、主播及直播间,按商家隔离。
-- =====================================================================================

-- 1. 抽奖活动表
CREATE TABLE IF NOT EXISTS public.ak_marketing_lotteries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL,                          -- 活动名称
    type INTEGER DEFAULT 1,                      -- 活动类型: 1积分抽奖, 2订单评价, 3订单支付
    
    start_time TIMESTAMPTZ NOT NULL,             -- 开始时间
    end_time TIMESTAMPTZ NOT NULL,               -- 结束时间
    
    is_open BOOLEAN DEFAULT true,                -- 是否开启
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 2. 抽奖奖品表
CREATE TABLE IF NOT EXISTS public.ak_marketing_lottery_prizes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    lottery_id UUID NOT NULL REFERENCES public.ak_marketing_lotteries(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL,                          -- 奖品名称
    prize_type TEXT NOT NULL,                    -- 奖品类型: points, balance, coupon, physical
    amount DECIMAL(12,2) DEFAULT 0,              -- 奖励面值/数量
    stock INTEGER DEFAULT 0,                     -- 奖品库存
    probability DECIMAL(5,2) DEFAULT 0,          -- 中奖概率 (0-100)
    
    sort_order INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT now()
);

-- 3. 直播主播表
CREATE TABLE IF NOT EXISTS public.ak_marketing_live_anchors (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    nickname TEXT NOT NULL,                      -- 主播昵称
    wechat TEXT,                                 -- 微信号
    phone TEXT,                                  -- 联系电话
    avatar_url TEXT,                             -- 头像
    
    status BOOLEAN DEFAULT true,                 -- 状态
    created_at TIMESTAMPTZ DEFAULT now()
);

-- 4. 直播间管理表
CREATE TABLE IF NOT EXISTS public.ak_marketing_live_rooms (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    anchor_id UUID REFERENCES public.ak_marketing_live_anchors(id) ON DELETE SET NULL,
    
    name TEXT NOT NULL,                          -- 直播间名称
    background_url TEXT,                         -- 背景图
    share_img_url TEXT,                          -- 分享图
    
    start_time TIMESTAMPTZ NOT NULL,             -- 开始时间
    end_time TIMESTAMPTZ NOT NULL,               -- 计划结束时间
    
    sort INTEGER DEFAULT 0,                      -- 排序
    type TEXT DEFAULT 'phone',                   -- 类型: phone手机直播等
    
    like_enabled BOOLEAN DEFAULT true,           -- 开启点赞
    sale_enabled BOOLEAN DEFAULT true,           -- 开启卖货
    comment_enabled BOOLEAN DEFAULT true,        -- 开启评论
    
    is_show BOOLEAN DEFAULT true,                -- 是否显示
    live_status INTEGER DEFAULT 1,               -- 1未开始, 2直播中, 3暂停, 4已结束
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 5. 启用 RLS
ALTER TABLE public.ak_marketing_lotteries ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_lottery_prizes ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_live_anchors ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_live_rooms ENABLE ROW LEVEL SECURITY;

-- 6. 创建权限策略 (按 merchant_id 隔离)
CREATE POLICY "Merchants manage their own lotteries" ON public.ak_marketing_lotteries FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
CREATE POLICY "Merchants manage their own anchors" ON public.ak_marketing_live_anchors FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
CREATE POLICY "Merchants manage their own rooms" ON public.ak_marketing_live_rooms FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());

-- 允许查看
CREATE POLICY "Users can view lotteries" ON public.ak_marketing_lotteries FOR SELECT TO authenticated USING (is_open = true);
CREATE POLICY "Users can view active rooms" ON public.ak_marketing_live_rooms FOR SELECT TO authenticated USING (is_show = true);
-- =====================================================================================
-- Schema: 打卡/签到增强配置表
-- 位置docs/sql/10_schema/marketing/ak_marketing_checkin_configs_v1.sql
-- 说明:管理打卡开关、模式、提醒及基础奖励(积分/经验),按商家隔离。
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_marketing_checkin_configs (
    id TEXT PRIMARY KEY DEFAULT 'checkin_config',
    merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    is_open BOOLEAN DEFAULT true,                -- 签到开关
    mode TEXT DEFAULT 'none',                    -- 签到模式: none(无限制), week(周循环), month(月循环)
    notice_enabled BOOLEAN DEFAULT false,        -- 签到提醒开关
    
    integral_reward INTEGER DEFAULT 10,          -- 每日签到赠送积分
    exp_reward INTEGER DEFAULT 1,                -- 每日签到赠送经验
    
    updated_at TIMESTAMPTZ DEFAULT now(),
    updated_by UUID REFERENCES auth.users(id)
);

-- 启用 RLS
ALTER TABLE public.ak_marketing_checkin_configs ENABLE ROW LEVEL SECURITY;

-- 权限策略:商家仅能管理自己的打卡配置
CREATE POLICY "Merchants manage their own checkin configs"
ON public.ak_marketing_checkin_configs FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许所有认证用户查看(用于前台展示)
CREATE POLICY "Anyone can view checkin config"
ON public.ak_marketing_checkin_configs FOR SELECT 
TO authenticated
USING (true);

-- 插入初始化数据(为每个管理员/商家初始化一条)
-- 实际应在商家创建时触发,此处先预留
-- =====================================================================================
-- Schema: 新人礼配置表
-- 位置docs/sql/10_schema/marketing/ak_marketing_newcomer_config_v1.sql
-- 说明:管理新用户注册后的奖励(余额、积分、优惠券),按商家隔离。
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_marketing_newcomer_config (
    id TEXT PRIMARY KEY DEFAULT 'newcomer_config',
    merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    balance_reward DECIMAL(12,2) DEFAULT 0.00,   -- 赠送余额
    integral_reward INTEGER DEFAULT 0,           -- 赠送积分
    
    -- 赠送优惠券 (JSONB 格式): [{ "id": "coupon_uuid", "name": "显示名称", "desc": "发放描述" }]
    coupons_json JSONB DEFAULT '[]'::jsonb, 
    
    updated_at TIMESTAMPTZ DEFAULT now(),
    updated_by UUID REFERENCES auth.users(id)
);

-- 启用 RLS
ALTER TABLE public.ak_marketing_newcomer_config ENABLE ROW LEVEL SECURITY;

-- 权限策略:商家仅能管理自己的新人礼配置
CREATE POLICY "Merchants manage their own newcomer configs"
ON public.ak_marketing_newcomer_config FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许查看配置(用于移动端展示)
CREATE POLICY "Anyone can view newcomer config"
ON public.ak_marketing_newcomer_config FOR SELECT 
TO authenticated
USING (true);
-- =====================================================================================
-- Schema: 签到记录表
-- 位置docs/sql/10_schema/marketing/ak_marketing_signin_logs_v1.sql
-- 说明:记录用户每日签到的详细流水,按商家隔离。
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_marketing_signin_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    points INTEGER NOT NULL DEFAULT 0,            -- 本次签到获得的积分
    is_continuous_reward BOOLEAN DEFAULT false,   -- 是否包含连续签到额外奖励
    
    created_at TIMESTAMPTZ DEFAULT now()
);

-- 启用 RLS
ALTER TABLE public.ak_marketing_signin_logs ENABLE ROW LEVEL SECURITY;

-- 权限策略:商家仅能管理/查看自己的签到记录
CREATE POLICY "Merchants manage their own signin logs"
ON public.ak_marketing_signin_logs FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许用户查看自己的签到记录
CREATE POLICY "Users view own signin logs"
ON public.ak_marketing_signin_logs FOR SELECT 
TO authenticated
USING (uid = auth.uid());

-- 索引
CREATE INDEX IF NOT EXISTS idx_signin_logs_merchant ON public.ak_marketing_signin_logs(merchant_id);
CREATE INDEX IF NOT EXISTS idx_signin_logs_uid ON public.ak_marketing_signin_logs(uid, created_at DESC);
-- =====================================================================================
-- Schema: 付费会员管理相关表
-- 位置docs/sql/10_schema/marketing/ak_member_management_v1.sql
-- 说明:管理会员卡类型、权益内容及基础配置,按商家隔离。
-- =====================================================================================

-- 1. 会员卡类型表
CREATE TABLE IF NOT EXISTS public.ak_marketing_member_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL,                          -- 会员名 (如: 月卡, 年卡)
    duration_days INTEGER DEFAULT 30,            -- 有效期0表示永久
    price DECIMAL(12,2) NOT NULL DEFAULT 0,      -- 原价
    discount_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 优惠价/实际支付价
    
    is_open BOOLEAN DEFAULT true,                -- 是否开启
    sort_order INTEGER DEFAULT 0,                -- 排序
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 2. 会员权益表
CREATE TABLE IF NOT EXISTS public.ak_marketing_member_rights (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL,                          -- 权益名称
    description TEXT,                            -- 权益简介
    icon_url TEXT,                               -- 权益图标
    
    is_show BOOLEAN DEFAULT true,                -- 是否展示
    sort_order INTEGER DEFAULT 0,                -- 排序
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 3. 会员基础配置表 (每个商家一条记录)
CREATE TABLE IF NOT EXISTS public.ak_marketing_member_config (
    id TEXT PRIMARY KEY DEFAULT 'member_config',
    merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    is_enabled BOOLEAN DEFAULT true,             -- 是否开启付费会员功能
    bg_img_url TEXT,                             -- 会员期内背景图
    expire_bg_img_url TEXT,                      -- 会员到期背景图
    rules_description TEXT,                      -- 会员规则说明文本
    
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 4. 启用 RLS
ALTER TABLE public.ak_marketing_member_types ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_member_rights ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_member_config ENABLE ROW LEVEL SECURITY;

-- 5. 创建权限策略 (按 merchant_id 隔离)
-- 商家管理自己的数据
CREATE POLICY "Merchants manage their own member types" ON public.ak_marketing_member_types FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
CREATE POLICY "Merchants manage their own member rights" ON public.ak_marketing_member_rights FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
CREATE POLICY "Merchants manage their own member config" ON public.ak_marketing_member_config FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());

-- 允许所有认证用户查看 (移动端展示)
CREATE POLICY "Users can view active member types" ON public.ak_marketing_member_types FOR SELECT TO authenticated USING (is_open = true);
CREATE POLICY "Users can view active member rights" ON public.ak_marketing_member_rights FOR SELECT TO authenticated USING (is_show = true);
CREATE POLICY "Users can view member config" ON public.ak_marketing_member_config FOR SELECT TO authenticated USING (true);

-- 6. 索引
CREATE INDEX IF NOT EXISTS idx_member_types_merchant ON public.ak_marketing_member_types(merchant_id);
CREATE INDEX IF NOT EXISTS idx_member_rights_merchant ON public.ak_marketing_member_rights(merchant_id);
-- =====================================================================================
-- Schema: 充值配置与额度模板表
-- 位置docs/sql/10_schema/marketing/ak_recharge_management_v1.sql
-- 说明:管理用户充值开关、最低金额及预设额度,按商家隔离。
-- =====================================================================================

-- 1. 充值基础配置表 (每个商家一条记录)
CREATE TABLE IF NOT EXISTS public.ak_recharge_configs (
    id TEXT PRIMARY KEY DEFAULT 'recharge_config',
    merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    balance_enabled BOOLEAN DEFAULT true,        -- 余额功能是否启用
    recharge_notice TEXT,                        -- 充值注意事项说明
    mp_recharge_enabled BOOLEAN DEFAULT false,   -- 小程序充值开关
    min_recharge_amount DECIMAL(12,2) DEFAULT 0.01, -- 最低充值金额
    
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 2. 充值额度模板表
CREATE TABLE IF NOT EXISTS public.ak_recharge_quotas (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    price DECIMAL(12,2) NOT NULL DEFAULT 0,      -- 售价(实际充值金额)
    bonus_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 赠送金额
    
    is_open BOOLEAN DEFAULT true,                -- 是否可用
    sort_order INTEGER DEFAULT 0,                -- 排序
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 3. 启用 RLS
ALTER TABLE public.ak_recharge_configs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_recharge_quotas ENABLE ROW LEVEL SECURITY;

-- 4. 创建权限策略 (按 merchant_id 隔离)
CREATE POLICY "Merchants manage their own recharge configs" ON public.ak_recharge_configs FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
CREATE POLICY "Merchants manage their own recharge quotas" ON public.ak_recharge_quotas FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());

-- 允许所有认证用户查看
CREATE POLICY "Users can view recharge configs" ON public.ak_recharge_configs FOR SELECT TO authenticated USING (true);
CREATE POLICY "Users can view active recharge quotas" ON public.ak_recharge_quotas FOR SELECT TO authenticated USING (is_open = true);

-- 5. 索引
CREATE INDEX IF NOT EXISTS idx_recharge_quotas_merchant ON public.ak_recharge_quotas(merchant_id);
-- =====================================================================================
-- Schema: 签到规则配置表
-- 位置docs/sql/10_schema/marketing/ak_signin_configs_v1.sql
-- 说明:记录每日签到积分、连续签到奖励及规则说明,按商家隔离。
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_signin_configs (
    id TEXT PRIMARY KEY DEFAULT 'signin_config', -- 每个商家一个配置记录
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    is_enabled BOOLEAN DEFAULT true,             -- 签到功能是否启用
    daily_points INTEGER DEFAULT 10,             -- 每日签到固定奖励积分
    
    -- 连续签到奖励 (JSONB 格式): [{ "day": 3, "points": 20 }, { "day": 7, "points": 50 }]
    continuous_rewards JSONB DEFAULT '[]'::jsonb, 
    
    rules_description TEXT DEFAULT '1.每日签到可获得积分奖励;\n2.连续签到满足天数可获得额外阶梯奖励;\n3.签到中断将重新从第一天开始计算。',
    
    updated_at TIMESTAMPTZ DEFAULT now(),
    updated_by UUID REFERENCES auth.users(id),

    -- 约束:同一个商家只有一个签到配置记录
    UNIQUE(merchant_id)
);

-- 启用 RLS
ALTER TABLE public.ak_signin_configs ENABLE ROW LEVEL SECURITY;

-- 权限策略:商家仅能管理自己的签到配置
CREATE POLICY "Merchants can manage their own signin configs"
ON public.ak_signin_configs
FOR ALL
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许所有认证用户查看配置(用于移动端签到展示)
CREATE POLICY "Authenticated users can view signin configs"
ON public.ak_signin_configs
FOR SELECT
TO authenticated
USING (true);
-- =====================================================================================
-- Schema Update: ml_orders 字段补齐
-- 位置docs/sql/10_schema/order/
-- 对象类型Schema (ALTER TABLE)
-- 版本v1
-- 说明:为订单主表补齐核销记录 RPC 所依赖的业务字段order_type, verified_at, verifier_id
-- =====================================================================================

DO $$ 
BEGIN
    -- 1. 补齐 order_type (1:普通, 2:收银, 3:核销)
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'order_type') THEN
        ALTER TABLE public.ml_orders ADD COLUMN order_type INTEGER DEFAULT 1;
        COMMENT ON COLUMN public.ml_orders.order_type IS '订单类型: 1:普通, 2:收银, 3:核销';
    END IF;

    -- 2. 补齐 verified_at (核销时间)
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'verified_at') THEN
        ALTER TABLE public.ml_orders ADD COLUMN verified_at TIMESTAMP WITH TIME ZONE;
        COMMENT ON COLUMN public.ml_orders.verified_at IS '核销时间';
    END IF;

    -- 3. 补齐 verifier_id (核销员ID)
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'verifier_id') THEN
        ALTER TABLE public.ml_orders ADD COLUMN verifier_id UUID REFERENCES public.ak_users(id);
        COMMENT ON COLUMN public.ml_orders.verifier_id IS '核销员ID';
    END IF;

    -- 4. 补齐 pay_type (支付方式: 1:余额, 2:微信, 3:支付宝, 4:线下支付)
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'pay_type') THEN
        ALTER TABLE public.ml_orders ADD COLUMN pay_type INTEGER DEFAULT 1;
        COMMENT ON COLUMN public.ml_orders.pay_type IS '支付方式: 1:余额, 2:微信, 3:支付宝, 4:线下支付';
    END IF;

    -- 5. 补齐 channel_type (订单渠道: 1:公众号, 2:小程序, 3:H5, 4:PC, 5:APP)
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'channel_type') THEN
        ALTER TABLE public.ml_orders ADD COLUMN channel_type INTEGER DEFAULT 1;
        COMMENT ON COLUMN public.ml_orders.channel_type IS '订单渠道: 1:公众号, 2:小程序, 3:H5, 4:PC, 5:APP';
    END IF;

END $$;
-- 1. 商品标签分组表
CREATE TABLE IF NOT EXISTS public.ak_product_label_groups (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    sort_order INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 2. 商品标签表
CREATE TABLE IF NOT EXISTS public.ak_product_labels (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    group_id UUID REFERENCES public.ak_product_label_groups(id) ON DELETE SET NULL,
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    is_active BOOLEAN DEFAULT true,
    show_in_mobile BOOLEAN DEFAULT true,
    sort_order INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 3. 启用 RLS
ALTER TABLE public.ak_product_label_groups ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_product_labels ENABLE ROW LEVEL SECURITY;

-- 4. 创建权限策略 (按 merchant_id 隔离)
-- 分组策略
CREATE POLICY "Users can manage their own label groups" 
ON public.ak_product_label_groups FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 标签策略
CREATE POLICY "Users can manage their own labels" 
ON public.ak_product_labels FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 5. 索引
CREATE INDEX IF NOT EXISTS idx_label_groups_merchant ON public.ak_product_label_groups(merchant_id);
CREATE INDEX IF NOT EXISTS idx_labels_group ON public.ak_product_labels(group_id);
CREATE INDEX IF NOT EXISTS idx_labels_merchant ON public.ak_product_labels(merchant_id);
-- =====================================================================================
-- Schema: 商品会员价表 (按 SKU + 等级 维度)
-- 位置docs/sql/10_schema/product/ak_product_member_prices_v1.sql
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:记录特定商品 SKU 在不同会员等级下的专享价格,按商家隔离。
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_product_member_prices (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_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 NOT NULL REFERENCES public.ml_product_skus(id) ON DELETE CASCADE,
    level_id UUID NOT NULL REFERENCES public.ak_user_levels(id) ON DELETE CASCADE,
    
    member_price DECIMAL(12,2) NOT NULL CHECK (member_price >= 0),
    
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now(),

    -- 约束:同一个商家的同一个 SKU 在同一个等级下只能有一个会员价
    UNIQUE(merchant_id, sku_id, level_id)
);

-- 索引
CREATE INDEX IF NOT EXISTS idx_prod_member_prices_product ON public.ak_product_member_prices(product_id);
CREATE INDEX IF NOT EXISTS idx_prod_member_prices_sku ON public.ak_product_member_prices(sku_id);
CREATE INDEX IF NOT EXISTS idx_prod_member_prices_level ON public.ak_product_member_prices(level_id);
CREATE INDEX IF NOT EXISTS idx_prod_member_prices_merchant ON public.ak_product_member_prices(merchant_id);

-- 启用 RLS
ALTER TABLE public.ak_product_member_prices ENABLE ROW LEVEL SECURITY;

-- 权限策略:商家仅能管理自己的商品会员价
CREATE POLICY "Merchants can manage their own product member prices"
ON public.ak_product_member_prices
FOR ALL
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许所有认证用户查看会员价(前台下单需计算)
CREATE POLICY "Authenticated users can view product member prices"
ON public.ak_product_member_prices
FOR SELECT
TO authenticated
USING (true);
-- 商品保障/服务条款(按 merchant_id 隔离)
CREATE TABLE IF NOT EXISTS public.ak_product_protections (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT NOT NULL DEFAULT '',
  icon_url TEXT,
  sort_order INTEGER DEFAULT 0,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_ak_product_protections_merchant ON public.ak_product_protections(merchant_id);
CREATE INDEX IF NOT EXISTS idx_ak_product_protections_active ON public.ak_product_protections(is_active);

ALTER TABLE public.ak_product_protections ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can manage their own product protections"
ON public.ak_product_protections
FOR ALL
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());
-- 商品规格模板表 + 商品参数模板表(按 merchant_id 隔离)

-- 1) 商品规格模板表
CREATE TABLE IF NOT EXISTS public.ak_product_spec_templates (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  specs TEXT NOT NULL DEFAULT '',
  attrs TEXT NOT NULL DEFAULT '',
  sort_order INTEGER DEFAULT 0,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_spec_templates_merchant ON public.ak_product_spec_templates(merchant_id);

ALTER TABLE public.ak_product_spec_templates ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can manage their own spec templates"
ON public.ak_product_spec_templates
FOR ALL
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());


-- 2) 商品参数模板表
CREATE TABLE IF NOT EXISTS public.ak_product_param_templates (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  sort_order INTEGER DEFAULT 0,
  params JSONB NOT NULL DEFAULT '[]'::jsonb,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_param_templates_merchant ON public.ak_product_param_templates(merchant_id);

ALTER TABLE public.ak_product_param_templates ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can manage their own param templates"
ON public.ak_product_param_templates
FOR ALL
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());
-- =====================================================================================
-- Schema: 运费模板表
-- 位置docs/sql/10_schema/product/ak_shipping_templates_v1.sql
-- 说明:管理商家的运费计算规则,按商家隔离。
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_shipping_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL,                          -- 模板名称
    calc_method TEXT DEFAULT 'piece',            -- 计费方式: piece(件数), weight(重量), volume(体积)
    is_free_shipping BOOLEAN DEFAULT false,      -- 是否包邮
    
    sort_order INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now()
);

-- 启用 RLS
ALTER TABLE public.ak_shipping_templates ENABLE ROW LEVEL SECURITY;

-- 权限策略:商家管理自己的模板
CREATE POLICY "Merchants manage own shipping templates" 
ON public.ak_shipping_templates FOR ALL 
TO authenticated
USING (merchant_id = auth.uid())
WITH CHECK (merchant_id = auth.uid());

-- 允许查看
CREATE POLICY "Authenticated users view shipping templates" 
ON public.ak_shipping_templates FOR SELECT 
TO authenticated
USING (true);

-- 索引
CREATE INDEX IF NOT EXISTS idx_shipping_templates_merchant ON public.ak_shipping_templates(merchant_id);
-- Schema: 医疗商城独立分类与商品分类关联
-- 执行阶段: 10_schema

CREATE TABLE IF NOT EXISTS public.medical_mall_categories (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    parent_id TEXT NULL REFERENCES public.medical_mall_categories(id),
    level INTEGER NOT NULL CHECK (level IN (1, 2)),
    sort_order INTEGER NOT NULL DEFAULT 0,
    icon TEXT,
    image_url TEXT,
    description TEXT,
    scene TEXT NOT NULL DEFAULT 'medical_mall',
    category_type TEXT NOT NULL DEFAULT 'normal',
    compliance_type TEXT NOT NULL DEFAULT 'normal',
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    deleted_at TIMESTAMPTZ NULL,
    deleted_by UUID NULL REFERENCES public.ak_users(id),
    restored_at TIMESTAMPTZ NULL,
    restored_by UUID NULL REFERENCES public.ak_users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

COMMENT ON TABLE public.medical_mall_categories IS '医疗商城独立分类表';
COMMENT ON COLUMN public.medical_mall_categories.scene IS '分类场景,如 medical_mall / home_mall';
COMMENT ON COLUMN public.medical_mall_categories.category_type IS '分类类型device / otc / care / rehab / elderly / nutrition / protection / tcm / all';
COMMENT ON COLUMN public.medical_mall_categories.compliance_type IS '合规类型normal / otc / device_class_i / device_class_ii / device_class_iii / rx_hidden';

CREATE INDEX IF NOT EXISTS idx_medical_mall_categories_parent
    ON public.medical_mall_categories(parent_id)
    WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS idx_medical_mall_categories_level_sort
    ON public.medical_mall_categories(level, sort_order)
    WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS idx_medical_mall_categories_scene_active
    ON public.medical_mall_categories(scene, is_active)
    WHERE deleted_at IS NULL;

CREATE TABLE IF NOT EXISTS public.medical_mall_product_categories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
    category_id TEXT NOT NULL REFERENCES public.medical_mall_categories(id),
    is_primary BOOLEAN NOT NULL DEFAULT FALSE,
    sort_order INTEGER NOT NULL DEFAULT 0,
    deleted_at TIMESTAMPTZ NULL,
    deleted_by UUID NULL REFERENCES public.ak_users(id),
    restored_at TIMESTAMPTZ NULL,
    restored_by UUID NULL REFERENCES public.ak_users(id),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (product_id, category_id)
);

COMMENT ON TABLE public.medical_mall_product_categories IS '医疗商城商品与分类关联表';

CREATE INDEX IF NOT EXISTS idx_medical_mall_product_categories_category
    ON public.medical_mall_product_categories(category_id, sort_order)
    WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS idx_medical_mall_product_categories_product
    ON public.medical_mall_product_categories(product_id)
    WHERE deleted_at IS NULL;-- =====================================================================================
-- Schema Update: ml_products 扩展字段 (物流、营销、高级设置)
-- 位置docs/sql/10_schema/product/ml_products_ext_v1.sql
-- 说明:补齐商品编辑页 Step 3-6 所需的持久化字段。
-- =====================================================================================

DO $$ 
BEGIN
    -- 1. 物流设置:关联运费模板
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'shipping_template_id') THEN
        ALTER TABLE public.ml_products ADD COLUMN shipping_template_id UUID REFERENCES public.ak_shipping_templates(id) ON DELETE SET NULL;
        COMMENT ON COLUMN public.ml_products.shipping_template_id IS '关联运费模板ID';
    END IF;

    -- 2. 营销设置:赠送积分
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'give_integral') THEN
        ALTER TABLE public.ml_products ADD COLUMN give_integral INTEGER DEFAULT 0;
        COMMENT ON COLUMN public.ml_products.give_integral IS '购买赠送积分';
    END IF;

    -- 3. 高级设置:警戒库存
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'stock_warning') THEN
        ALTER TABLE public.ml_products ADD COLUMN stock_warning INTEGER DEFAULT 10;
        COMMENT ON COLUMN public.ml_products.stock_warning IS '库存报警数值';
    END IF;

    -- 4. 高级设置:虚拟销量
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'virtual_sales') THEN
        ALTER TABLE public.ml_products ADD COLUMN virtual_sales INTEGER DEFAULT 0;
        COMMENT ON COLUMN public.ml_products.virtual_sales IS '虚拟销量(展示用)';
    END IF;

    -- 5. 高级设置:排序
    IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'sort_order') THEN
        ALTER TABLE public.ml_products ADD COLUMN sort_order INTEGER DEFAULT 0;
        COMMENT ON COLUMN public.ml_products.sort_order IS '商品排序权重';
    END IF;

END $$;
-- =====================================================================================
-- Schema: 权限管理 (RBAC) 核心表
-- 位置docs/sql/10_schema/user/ak_auth_system_v1.sql
-- 对象类型TABLE
-- 版本v1
-- 说明:包含角色表、权限/菜单表及用户角色关联表
-- =====================================================================================

-- 1. 角色表
CREATE TABLE IF NOT EXISTS public.ak_roles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL UNIQUE,      -- 角色名称 (如: 超级管理员)
    code TEXT NOT NULL UNIQUE,      -- 角色编码 (如: super_admin)
    description TEXT,               -- 角色描述
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 2. 权限/菜单表
CREATE TABLE IF NOT EXISTS public.ak_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    parent_id UUID REFERENCES public.ak_permissions(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL,             -- 权限/菜单名称
    code TEXT NOT NULL UNIQUE,      -- 权限编码 (如: order_view)
    type TEXT NOT NULL,             -- 类型: menu(菜单), button(按钮/接口)
    
    path TEXT,                      -- 前端路由路径 (仅针对 menu)
    icon TEXT,                      -- 图标
    sort_order INTEGER DEFAULT 0,   -- 排序
    
    is_visible BOOLEAN DEFAULT TRUE, -- 菜单是否在左侧可见
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 3. 用户-角色关联表
-- 映射管理员 (ak_users) 与角色
CREATE TABLE IF NOT EXISTS public.ak_admin_roles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
    role_id UUID NOT NULL REFERENCES public.ak_roles(id) ON DELETE CASCADE,
    
    assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    assigned_by UUID REFERENCES public.ak_users(id),
    
    UNIQUE(user_id, role_id)
);

-- 4. 角色-权限关联表
CREATE TABLE IF NOT EXISTS public.ak_role_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    role_id UUID NOT NULL REFERENCES public.ak_roles(id) ON DELETE CASCADE,
    permission_id UUID NOT NULL REFERENCES public.ak_permissions(id) ON DELETE CASCADE,
    
    UNIQUE(role_id, permission_id)
);

-- 索引
CREATE INDEX IF NOT EXISTS idx_permissions_parent_id ON public.ak_permissions(parent_id);
CREATE INDEX IF NOT EXISTS idx_admin_roles_user_id ON public.ak_admin_roles(user_id);
CREATE INDEX IF NOT EXISTS idx_role_permissions_role_id ON public.ak_role_permissions(role_id);

-- 注释
COMMENT ON TABLE public.ak_roles IS '后台管理角色表';
COMMENT ON TABLE public.ak_permissions IS '功能权限与菜单定义表';
COMMENT ON TABLE public.ak_admin_roles IS '管理员角色分配表';
COMMENT ON TABLE public.ak_role_permissions IS '角色权限映射表';
-- =====================================================================================
-- Schema: 用户分组表
-- 位置docs/sql/10_schema/user/ak_user_groups_v1.sql
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:用户分组定义,支持逻辑删除和状态管理
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_user_groups (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  
  name TEXT NOT NULL,
  remark TEXT NULL,
  status INT NOT NULL DEFAULT 1, -- 1:启用, 0:禁用
  
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  deleted_at TIMESTAMPTZ NULL,
  
  CONSTRAINT ak_user_groups_name_length CHECK (char_length(name) >= 1)
);

-- 唯一性约束(仅对未删除记录生效)
CREATE UNIQUE INDEX IF NOT EXISTS ak_user_groups_name_uniq_active 
  ON public.ak_user_groups (name) 
  WHERE deleted_at IS NULL;

-- 常用查询索引
CREATE INDEX IF NOT EXISTS ak_user_groups_status_idx ON public.ak_user_groups (status) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS ak_user_groups_created_at_idx ON public.ak_user_groups (created_at DESC);
-- =====================================================================================
-- Schema: 用户标签表
-- 位置docs/sql/10_schema/user/ak_user_labels_v1.sql
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:用户标签定义,支持逻辑删除与状态管理
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_user_labels (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  name TEXT NOT NULL,
  color TEXT NULL,
  remark TEXT NULL,
  status INT NOT NULL DEFAULT 1,

  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  deleted_at TIMESTAMPTZ NULL,

  CONSTRAINT ak_user_labels_name_length CHECK (char_length(name) >= 1)
);

CREATE UNIQUE INDEX IF NOT EXISTS ak_user_labels_name_uniq_active
  ON public.ak_user_labels (name)
  WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS ak_user_labels_status_idx
  ON public.ak_user_labels (status)
  WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS ak_user_labels_created_at_idx
  ON public.ak_user_labels (created_at DESC);
-- =====================================================================================
-- Schema: 用户等级表
-- 位置docs/sql/10_schema/user/
-- 对象类型Schema (DDL)
-- 版本v1
-- 说明:用户等级(经验值/折扣/展示/状态),支持逻辑删除
-- =====================================================================================

CREATE TABLE IF NOT EXISTS public.ak_user_levels (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  name TEXT NOT NULL,
  level_weight INT NOT NULL,
  min_experience INT NOT NULL DEFAULT 0,
  discount_percent INT NOT NULL DEFAULT 100,

  is_visible BOOLEAN NOT NULL DEFAULT TRUE,
  status INT NOT NULL DEFAULT 1,

  icon_url TEXT NULL,
  bg_image_url TEXT NULL,
  bg_style_json JSONB NULL,

  remark TEXT NULL,

  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  deleted_at TIMESTAMPTZ NULL,

  CONSTRAINT ak_user_levels_level_weight_nonnegative CHECK (level_weight >= 0),
  CONSTRAINT ak_user_levels_min_experience_nonnegative CHECK (min_experience >= 0),
  CONSTRAINT ak_user_levels_discount_percent_range CHECK (discount_percent BETWEEN 1 AND 100)
);

-- 唯一性(仅对未删除记录生效)
CREATE UNIQUE INDEX IF NOT EXISTS ak_user_levels_name_uniq_active
  ON public.ak_user_levels (name)
  WHERE deleted_at IS NULL;

CREATE UNIQUE INDEX IF NOT EXISTS ak_user_levels_level_weight_uniq_active
  ON public.ak_user_levels (level_weight)
  WHERE deleted_at IS NULL;

-- 常用查询索引
CREATE INDEX IF NOT EXISTS ak_user_levels_active_filter_idx
  ON public.ak_user_levels (status, is_visible)
  WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS ak_user_levels_min_experience_idx
  ON public.ak_user_levels (min_experience)
  WHERE deleted_at IS NULL;

CREATE INDEX IF NOT EXISTS ak_user_levels_level_weight_desc_idx
  ON public.ak_user_levels (level_weight DESC)
  WHERE deleted_at IS NULL;
-- Schema Update: public.ak_users 增加真实姓名与手机号字段(用于分销/推广员等管理端展示)
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema = 'public' AND table_name = 'ak_users' AND column_name = 'real_name'
  ) THEN
    ALTER TABLE public.ak_users ADD COLUMN real_name text;
    COMMENT ON COLUMN public.ak_users.real_name IS '真实姓名(可选)';
  END IF;

  IF NOT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema = 'public' AND table_name = 'ak_users' AND column_name = 'phone'
  ) THEN
    ALTER TABLE public.ak_users ADD COLUMN phone text;
    COMMENT ON COLUMN public.ak_users.phone IS '手机号(可选)';
  END IF;
END $$;
-- =====================================================================================
-- Schema: public.ak_users
-- Version: v1
-- Purpose: 修复 auth.users -> ak_users 自动同步的写入协调问题
-- Change: 放宽 username 和 email 的 NOT NULL 约束,以允许数据库触发器成功插入新用户记录。
--         同时,将 role 的默认值更新为 'customer' 以符合业务逻辑。
-- =====================================================================================

BEGIN;

-- 步骤 1 & 2: 允许 username/email 为空,并更新 role 默认值
-- 这样数据库的自动用户同步触发器就不会因为缺少 NOT NULL 的值而失败。
-- 前端代码 (ensureUserProfile) 会在用户首次登录时尝试填充这些值。
ALTER TABLE public.ak_users
  ALTER COLUMN username DROP NOT NULL,
  ALTER COLUMN email DROP NOT NULL,
  ALTER COLUMN role SET DEFAULT 'customer';

COMMIT;
-- =====================================================================================
-- User 模块扩展 - 财务字段补全
-- 位置docs/sql/10_schema/user/
-- 版本v1
-- 描述:为 ak_users 增加余额与佣金字段,支持财务业务。
-- =====================================================================================

ALTER TABLE public.ak_users 
ADD COLUMN IF NOT EXISTS now_money DECIMAL(12,2) DEFAULT 0,
ADD COLUMN IF NOT EXISTS brokerage_price DECIMAL(12,2) DEFAULT 0;

COMMENT ON COLUMN public.ak_users.now_money IS '用户当前余额';
COMMENT ON COLUMN public.ak_users.brokerage_price IS '用户当前佣金';

3) RLS行级安全

-- =====================================================================================
-- RLS: 系统配置表安全策略
-- 位置docs/sql/20_rls/admin/ml_system_configs_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:允许所有登录用户读取配置;管理端全量操作通过 RPC (SECURITY DEFINER) 执行
-- =====================================================================================

-- 启用 RLS
ALTER TABLE public.ml_system_configs ENABLE ROW LEVEL SECURITY;

-- 1. 允许所有登录用户读取配置 (用于前端业务逻辑判断)
DROP POLICY IF EXISTS system_configs_select_policy ON public.ml_system_configs;
CREATE POLICY system_configs_select_policy ON public.ml_system_configs 
FOR SELECT TO authenticated USING (deleted_at IS NULL);

-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行,此处不再额外开放直接表操作
-- =====================================================================================
-- RLS: 权限管理 (Auth) 安全策略
-- 位置docs/sql/20_rls/auth/ak_auth_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:角色与权限表默认不对外开放,全量管理通过 SECURITY DEFINER RPC 执行
-- =====================================================================================

-- 启用 RLS
ALTER TABLE public.ak_roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_permissions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_admin_roles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_role_permissions ENABLE ROW LEVEL SECURITY;

-- 默认策略NO DIRECT ACCESS
-- 所有的查询和修改均建议通过 docs/sql/30_rpc/auth/ 下的专用管理接口完成
-- 这样可以确保鉴权逻辑与 ak_users.role 强制绑定,且具备审计能力
-- =====================================================================================
-- RLS: 内容管理模块安全策略
-- 位置docs/sql/20_rls/cms/ml_cms_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:消费者端可读(仅已发布/启用);管理端通过 RPC 访问
-- =====================================================================================

-- 1. 开启 RLS
ALTER TABLE public.ml_article_categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_articles ENABLE ROW LEVEL SECURITY;

-- 2. 分类表策略:允许所有人读取启用的分类
DROP POLICY IF EXISTS ml_article_categories_select_active ON public.ml_article_categories;
CREATE POLICY ml_article_categories_select_active
  ON public.ml_article_categories
  FOR SELECT
  TO anon, authenticated
  USING (status = 1 AND deleted_at IS NULL);

-- 3. 文章表策略:允许所有人读取已发布的文章
DROP POLICY IF EXISTS ml_articles_select_published ON public.ml_articles;
CREATE POLICY ml_articles_select_published
  ON public.ml_articles
  FOR SELECT
  TO anon, authenticated
  USING (status = 1 AND deleted_at IS NULL);

-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,管理端操作通过 RPC (SECURITY DEFINER) 执行
-- =====================================================================================
-- RLS: 装修模块 - DIY 页面安全策略
-- 位置docs/sql/20_rls/decoration/ml_decoration_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:消费者端公开只读已发布的页面;管理端通过 SECURITY DEFINER RPC 进行管理
-- =====================================================================================

-- 1. 启用 RLS
ALTER TABLE public.ak_diy_pages ENABLE ROW LEVEL SECURITY;

-- 2. 消费者端策略:允许匿名和登录用户读取已启用的页面
DROP POLICY IF EXISTS diy_pages_select_active ON public.ak_diy_pages;
CREATE POLICY diy_pages_select_active ON public.ak_diy_pages 
FOR SELECT TO anon, authenticated 
USING (is_active = true AND deleted_at IS NULL);

-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行,此处不再额外开放直接表操作
-- =====================================================================================
-- RLS: 物流设置 (Delivery) 安全策略
-- 位置docs/sql/20_rls/delivery/ak_delivery_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:配送员表管理端私有;提货点表消费者端只读
-- =====================================================================================

-- 启用 RLS
ALTER TABLE public.ml_delivery_staff ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_delivery_stations ENABLE ROW LEVEL SECURITY;

-- 1. 配送员表策略:默认不开放直接访问
-- 全量管理通过 docs/sql/30_rpc/delivery/ 下的 RPC 执行

-- 2. 提货点表策略:允许消费者端只读(用于地图展示和下单选择)
DROP POLICY IF EXISTS delivery_stations_select_active ON public.ml_delivery_stations;
CREATE POLICY delivery_stations_select_active
  ON public.ml_delivery_stations
  FOR SELECT
  TO anon, authenticated
  USING (status = 1 AND deleted_at IS NULL);

-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行
-- =====================================================================================
-- RLS: 医养执行端 Delivery 安全策略升级
-- 位置docs/sql/20_rls/delivery/ak_delivery_rls_v2.sql
-- 对象类型RLS 策略
-- 版本v2
-- 说明:保留管理端通过 SECURITY DEFINER RPC 管理,补充执行人员本人直读自己档案。
-- =====================================================================================

ALTER TABLE public.ml_delivery_staff ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_delivery_stations ENABLE ROW LEVEL SECURITY;

-- 清理旧策略
DROP POLICY IF EXISTS delivery_staff_self_select ON public.ml_delivery_staff;
DROP POLICY IF EXISTS delivery_staff_self_update ON public.ml_delivery_staff;
DROP POLICY IF EXISTS delivery_stations_select_active ON public.ml_delivery_stations;

-- 1. 执行人员本人可直读自己的未删除档案
CREATE POLICY delivery_staff_self_select
  ON public.ml_delivery_staff
  FOR SELECT
  TO authenticated
  USING (
    deleted_at IS NULL
    AND EXISTS (
      SELECT 1
      FROM public.ak_users u
      WHERE u.id = ml_delivery_staff.uid
        AND u.auth_id = auth.uid()
    )
  );

-- 2. 执行人员本人可更新自己的在线状态等自有档案字段
CREATE POLICY delivery_staff_self_update
  ON public.ml_delivery_staff
  FOR UPDATE
  TO authenticated
  USING (
    deleted_at IS NULL
    AND EXISTS (
      SELECT 1
      FROM public.ak_users u
      WHERE u.id = ml_delivery_staff.uid
        AND u.auth_id = auth.uid()
    )
  )
  WITH CHECK (
    deleted_at IS NULL
    AND EXISTS (
      SELECT 1
      FROM public.ak_users u
      WHERE u.id = ml_delivery_staff.uid
        AND u.auth_id = auth.uid()
    )
  );

-- 3. 提货点/机构对前台保持只读,仅返回启用且未删除数据
CREATE POLICY delivery_stations_select_active
  ON public.ml_delivery_stations
  FOR SELECT
  TO anon, authenticated
  USING (status = 1 AND deleted_at IS NULL);

-- 4. 派单候选人读取统一走 SECURITY DEFINER RPC不再开放公开可派单列表 RLS
-- 5. 其余直连写操作默认不开放,管理端统一走 SECURITY DEFINER RPC
-- =====================================================================================
-- RLS: 分销模块安全策略
-- 位置docs/sql/20_rls/distribution/ml_distribution_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:管理端全量权限通过 SECURITY DEFINER RPC 执行;用户仅能访问个人关联数据
-- =====================================================================================

-- 启用 RLS
ALTER TABLE public.ak_distribution_config ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_distribution_level ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_promoter_relations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_commission_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_distribution_divisions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_distribution_agents ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_distribution_agent_applications ENABLE ROW LEVEL SECURITY;

-- 1. 分销配置:允许所有登录用户读取(消费者端展示逻辑需要)
DROP POLICY IF EXISTS dist_config_select_policy ON public.ak_distribution_config;
CREATE POLICY dist_config_select_policy ON public.ak_distribution_config 
FOR SELECT TO authenticated USING (deleted_at IS NULL);

-- 2. 分销等级:允许所有登录用户读取可见等级
DROP POLICY IF EXISTS dist_level_select_policy ON public.ak_distribution_level;
CREATE POLICY dist_level_select_policy ON public.ak_distribution_level 
FOR SELECT TO authenticated USING (is_visible = true AND deleted_at IS NULL);

-- 3. 推广员关系:用户仅能查看与自己相关的记录
DROP POLICY IF EXISTS promoter_relations_select_policy ON public.ak_promoter_relations;
CREATE POLICY promoter_relations_select_policy ON public.ak_promoter_relations 
FOR SELECT TO authenticated USING ((uid = auth.uid() OR inviter_uid = auth.uid()) AND deleted_at IS NULL);

-- 4. 佣金日志:用户仅能查看自己的佣金记录
DROP POLICY IF EXISTS commission_logs_select_policy ON public.ak_commission_logs;
CREATE POLICY commission_logs_select_policy ON public.ak_commission_logs 
FOR SELECT TO authenticated USING (uid = auth.uid() AND deleted_at IS NULL);

-- 5. 事业部与代理商:允许登录用户查看启用的记录
DROP POLICY IF EXISTS dist_divisions_select_policy ON public.ak_distribution_divisions;
CREATE POLICY dist_divisions_select_policy ON public.ak_distribution_divisions 
FOR SELECT TO authenticated USING (is_enabled = true AND deleted_at IS NULL);

DROP POLICY IF EXISTS dist_agents_select_policy ON public.ak_distribution_agents;
CREATE POLICY dist_agents_select_policy ON public.ak_distribution_agents 
FOR SELECT TO authenticated USING (is_enabled = true AND deleted_at IS NULL);

-- 6. 代理商申请:用户仅能管理自己的申请记录
DROP POLICY IF EXISTS dist_apply_user_policy ON public.ak_distribution_agent_applications;
CREATE POLICY dist_apply_user_policy ON public.ak_distribution_agent_applications 
FOR ALL TO authenticated USING (uid = auth.uid() AND deleted_at IS NULL) WITH CHECK (uid = auth.uid());

-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行,此处不再额外开放直接表操作
-- =====================================================================================
-- RLS: 用户提现申请表
-- 位置docs/sql/20_rls/finance/
-- 对象类型RLS 策略
-- 版本v1
-- 说明:仅允许用户查看自己的提现记录;管理端通过 RPC 访问
-- =====================================================================================

ALTER TABLE public.ml_extract ENABLE ROW LEVEL SECURITY;

-- 策略 1: 允许用户读取自己的提现申请
DROP POLICY IF EXISTS ml_extract_user_select ON public.ml_extract;
CREATE POLICY ml_extract_user_select
  ON public.ml_extract
  FOR SELECT
  TO authenticated
  USING (uid = auth.uid() AND deleted_at IS NULL);

-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户
-- 提现申请通常由特定的 RPC 函数 (security definer) 创建,以确保业务逻辑(如冻结余额)的原子性
-- =====================================================================================
-- RLS: 发票管理表
-- 位置docs/sql/20_rls/finance/ml_invoices_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:用户仅能查看自己的开票申请;管理端通过 RPC 访问
-- =====================================================================================

ALTER TABLE public.ml_invoices ENABLE ROW LEVEL SECURITY;

-- 策略 1: 允许用户读取自己的记录(仅未删除数据)
DROP POLICY IF EXISTS ml_invoices_user_select ON public.ml_invoices;
CREATE POLICY ml_invoices_user_select
  ON public.ml_invoices
  FOR SELECT
  TO authenticated
  USING (uid = auth.uid() AND deleted_at IS NULL);

-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,通常由 RPC 或支付后逻辑触发
-- =====================================================================================
-- RLS: 用户资金流水表
-- 位置docs/sql/20_rls/finance/
-- 对象类型RLS 策略
-- 版本v1
-- 说明:仅允许用户查看自己的流水记录;管理端通过 RPC 访问
-- =====================================================================================

ALTER TABLE public.ml_user_bill ENABLE ROW LEVEL SECURITY;

-- 策略 1: 允许用户读取自己的记录(仅未删除数据)
DROP POLICY IF EXISTS ml_user_bill_user_select ON public.ml_user_bill;
CREATE POLICY ml_user_bill_user_select
  ON public.ml_user_bill
  FOR SELECT
  TO authenticated
  USING (uid = auth.uid() AND deleted_at IS NULL);

-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,由后端逻辑或 RPC 触发
-- =====================================================================================
-- RLS: 用户充值记录表
-- 位置docs/sql/20_rls/finance/
-- 对象类型RLS 策略
-- 版本v1
-- 说明:仅允许用户查看自己的充值记录;管理端通过 RPC 访问
-- =====================================================================================

ALTER TABLE public.ml_user_recharge ENABLE ROW LEVEL SECURITY;

-- 策略 1: 允许用户读取自己的记录(仅未删除数据)
DROP POLICY IF EXISTS ml_user_recharge_user_select ON public.ml_user_recharge;
CREATE POLICY ml_user_recharge_user_select
  ON public.ml_user_recharge
  FOR SELECT
  TO authenticated
  USING (uid = auth.uid() AND deleted_at IS NULL);

-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,写操作通常由业务逻辑或支付回调触发
-- =====================================================================================
-- RLS: 客服模块安全策略
-- 位置docs/sql/20_rls/kefu/ml_kefu_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:管理端全量访问通过 RPC 完成;用户仅能操作自己的留言反馈
-- =====================================================================================

-- 开启所有表的 RLS
ALTER TABLE public.ml_kefu_accounts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_kefu_word_categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_kefu_words ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_kefu_feedbacks ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ml_kefu_auto_replies ENABLE ROW LEVEL SECURITY;

-- 1. 留言反馈表策略
-- 允许登录用户插入自己的留言
DROP POLICY IF EXISTS ml_kefu_feedbacks_user_insert ON public.ml_kefu_feedbacks;
CREATE POLICY ml_kefu_feedbacks_user_insert
  ON public.ml_kefu_feedbacks
  FOR INSERT
  TO authenticated
  WITH CHECK (user_id = auth.uid());

-- 允许用户查看自己的留言
DROP POLICY IF EXISTS ml_kefu_feedbacks_user_select ON public.ml_kefu_feedbacks;
CREATE POLICY ml_kefu_feedbacks_user_select
  ON public.ml_kefu_feedbacks
  FOR SELECT
  TO authenticated
  USING (user_id = auth.uid());

-- 其他表(账号、话术、自动回复)默认不向 anon/authenticated 角色开放 SELECT/INSERT/UPDATE/DELETE
-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 函数执行
-- =====================================================================================
-- RLS Policy: 优惠券模板表权限控制
-- 位置docs/sql/20_rls/marketing/ml_coupon_templates_rls_v1.sql
-- 说明:确保商家仅能管理自己的优惠券模板,管理员拥有全权限。
-- =====================================================================================

-- 1. 启用 RLS
ALTER TABLE public.ml_coupon_templates ENABLE ROW LEVEL SECURITY;

-- 2. 创建权限策略
-- 允许商家管理自己的模板
CREATE POLICY ml_coupon_templates_merchant_policy ON public.ml_coupon_templates
    FOR ALL 
    TO authenticated
    USING (
        merchant_id = auth.uid() OR 
        EXISTS (
            SELECT 1 FROM public.ak_users 
            WHERE id = auth.uid() AND role = 'admin'
        )
    )
    WITH CHECK (
        merchant_id = auth.uid() OR 
        EXISTS (
            SELECT 1 FROM public.ak_users 
            WHERE id = auth.uid() AND role = 'admin'
        )
    );

-- 允许所有认证用户查看模板(用于前台领取)
CREATE POLICY ml_coupon_templates_select_policy ON public.ml_coupon_templates
    FOR SELECT 
    TO authenticated
    USING (status = 1);
-- =====================================================================================
-- RLS: 营销核心活动表安全策略
-- 位置docs/sql/20_rls/marketing/ml_marketing_activities_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:消费者端公开只读;管理端操作由 RPC (SECURITY DEFINER) 承载
-- =====================================================================================

-- 启用 RLS
ALTER TABLE public.ak_seckill_activities ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_combination_activities ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_bargains ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_groupbuys ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_lotteries ENABLE ROW LEVEL SECURITY;

-- 1. 秒杀活动:公开只读
DROP POLICY IF EXISTS seckill_select_policy ON public.ak_seckill_activities;
CREATE POLICY seckill_select_policy ON public.ak_seckill_activities FOR SELECT TO anon, authenticated USING (status = true);

-- 2. 拼团活动:公开只读
DROP POLICY IF EXISTS combination_select_policy ON public.ak_combination_activities;
CREATE POLICY combination_select_policy ON public.ak_combination_activities FOR SELECT TO anon, authenticated USING (status = 'ongoing');

-- 3. 砍价活动:公开只读
DROP POLICY IF EXISTS bargain_select_policy ON public.ak_marketing_bargains;
CREATE POLICY bargain_select_policy ON public.ak_marketing_bargains FOR SELECT TO anon, authenticated USING (status = true);

-- 4. 团购活动:公开只读
DROP POLICY IF EXISTS groupbuy_select_policy ON public.ak_marketing_groupbuys;
CREATE POLICY groupbuy_select_policy ON public.ak_marketing_groupbuys FOR SELECT TO anon, authenticated USING (status = true);

-- 5. 抽奖活动:公开只读
DROP POLICY IF EXISTS lottery_select_policy ON public.ak_marketing_lotteries;
CREATE POLICY lottery_select_policy ON public.ak_marketing_lotteries FOR SELECT TO anon, authenticated USING (is_open = true);

-- 管理端全量管理将由 SECURITY DEFINER 的 RPC 接口执行
-- =====================================================================================
-- RLS: 营销模块其他业务表安全策略 (互动/会员/直播/充值)
-- 位置docs/sql/20_rls/marketing/ml_marketing_others_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:配置类公开只读;记录类用户隔离;管理端由 RPC 承载
-- =====================================================================================

-- 启用 RLS
ALTER TABLE public.ak_signin_configs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_signin_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_newcomer_config ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_member_types ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_member_rights ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_member_config ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_live_anchors ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_marketing_live_rooms ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_recharge_configs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ak_recharge_quotas ENABLE ROW LEVEL SECURITY;

-- 1. 签到与新人礼配置:公开只读
DROP POLICY IF EXISTS signin_config_select_policy ON public.ak_signin_configs;
CREATE POLICY signin_config_select_policy ON public.ak_signin_configs FOR SELECT TO authenticated USING (true);

DROP POLICY IF EXISTS newcomer_config_select_policy ON public.ak_marketing_newcomer_config;
CREATE POLICY newcomer_config_select_policy ON public.ak_marketing_newcomer_config FOR SELECT TO authenticated USING (true);

-- 2. 签到日志:用户仅能查看自己的
DROP POLICY IF EXISTS signin_logs_user_policy ON public.ak_marketing_signin_logs;
CREATE POLICY signin_logs_user_policy ON public.ak_marketing_signin_logs 
FOR SELECT TO authenticated USING (uid = auth.uid());

-- 3. 会员体系:类型与权益公开只读
DROP POLICY IF EXISTS member_types_select_policy ON public.ak_marketing_member_types;
CREATE POLICY member_types_select_policy ON public.ak_marketing_member_types FOR SELECT TO authenticated USING (is_open = true);

DROP POLICY IF EXISTS member_rights_select_policy ON public.ak_marketing_member_rights;
CREATE POLICY member_rights_select_policy ON public.ak_marketing_member_rights FOR SELECT TO authenticated USING (is_show = true);

DROP POLICY IF EXISTS member_config_select_policy ON public.ak_marketing_member_config;
CREATE POLICY member_config_select_policy ON public.ak_marketing_member_config FOR SELECT TO authenticated USING (is_enabled = true);

-- 4. 直播:公开只读
DROP POLICY IF EXISTS live_anchors_select_policy ON public.ak_marketing_live_anchors;
CREATE POLICY live_anchors_select_policy ON public.ak_marketing_live_anchors FOR SELECT TO authenticated USING (status = true);

DROP POLICY IF EXISTS live_rooms_select_policy ON public.ak_marketing_live_rooms;
CREATE POLICY live_rooms_select_policy ON public.ak_marketing_live_rooms FOR SELECT TO authenticated USING (is_show = true);

-- 5. 充值配置:公开只读
DROP POLICY IF EXISTS recharge_config_select_policy ON public.ak_recharge_configs;
CREATE POLICY recharge_config_select_policy ON public.ak_recharge_configs FOR SELECT TO authenticated USING (balance_enabled = true);

DROP POLICY IF EXISTS recharge_quotas_select_policy ON public.ak_recharge_quotas;
CREATE POLICY recharge_quotas_select_policy ON public.ak_recharge_quotas FOR SELECT TO authenticated USING (is_open = true);

-- 管理端全量管理均通过 SECURITY DEFINER 的 RPC 接口执行
-- RLS: 医疗商城独立分类与商品分类关联
-- 执行阶段: 20_rls

ALTER TABLE public.medical_mall_categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.medical_mall_product_categories ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS medical_mall_categories_public_select_active ON public.medical_mall_categories;
CREATE POLICY medical_mall_categories_public_select_active
ON public.medical_mall_categories
FOR SELECT
TO anon, authenticated
USING (
    deleted_at IS NULL
    AND is_active = TRUE
    AND compliance_type <> 'rx_hidden'
);

DROP POLICY IF EXISTS medical_mall_product_categories_public_select_active ON public.medical_mall_product_categories;
CREATE POLICY medical_mall_product_categories_public_select_active
ON public.medical_mall_product_categories
FOR SELECT
TO anon, authenticated
USING (deleted_at IS NULL);-- =====================================================================================
-- RLS: 用户分组表
-- 位置docs/sql/20_rls/user/ak_user_groups_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:管理端全量访问通过 RPC 完成;消费者端默认不开放直接访问
-- =====================================================================================

ALTER TABLE public.ak_user_groups ENABLE ROW LEVEL SECURITY;

-- 如果未来消费者端需要展示所在分组,可以在此添加对应的 SELECT 策略
-- 目前默认不向普通用户开放任何直接 SQL 读写权限
-- =====================================================================================
-- RLS: 用户标签表
-- 位置docs/sql/20_rls/user/ak_user_labels_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:管理端全量访问通过 RPC 完成;消费者端默认不开放直接访问
-- =====================================================================================

ALTER TABLE public.ak_user_labels ENABLE ROW LEVEL SECURITY;

-- 若后续消费者端需要展示标签,可在此添加 SELECT 策略
-- 当前默认不向普通用户开放任何直接 SQL 读写权限
-- =====================================================================================
-- RLS: 用户等级表
-- 位置docs/sql/20_rls/user/
-- 对象类型RLS 策略
-- 版本v1
-- 说明:消费者端可读(仅可见/启用/未删除);管理端全量访问通过 RPC 完成
-- =====================================================================================

ALTER TABLE public.ak_user_levels ENABLE ROW LEVEL SECURITY;

-- 消费者端:允许读取可见且启用的等级(未删除)
DROP POLICY IF EXISTS ak_user_levels_public_select_visible_active ON public.ak_user_levels;
CREATE POLICY ak_user_levels_public_select_visible_active
  ON public.ak_user_levels
  FOR SELECT
  TO anon, authenticated
  USING (
    deleted_at IS NULL
    AND status = 1
    AND is_visible = TRUE
  );

-- 默认不开放写权限INSERT/UPDATE/DELETE给 anon/authenticated

4) RPC关键函数完整见 30_rpc 目录)

-- =====================================================================================
-- Admin 统计功能 - 获取全站核心指标概览 RPC
-- 位置docs/sql/30_rpc/admin/rpc_admin_get_overall_stats_v1.sql
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:一次性聚合查询销售、订单、用户及商品的核心统计指标
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_get_overall_stats()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_stats JSONB;
    v_today_start TIMESTAMPTZ := CURRENT_DATE;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 聚合统计
    WITH totals AS (
        SELECT 
            (SELECT COALESCE(SUM(paid_amount), 0) FROM public.ml_orders WHERE paid = 1) as total_sales,
            (SELECT COUNT(*) FROM public.ml_orders WHERE paid = 1) as total_orders,
            (SELECT COUNT(*) FROM public.ak_users) as total_users,
            (SELECT COUNT(*) FROM public.ml_products) as total_products
    ),
    today_stats AS (
        SELECT 
            (SELECT COALESCE(SUM(paid_amount), 0) FROM public.ml_orders WHERE paid = 1 AND created_at >= v_today_start) as today_sales,
            (SELECT COUNT(*) FROM public.ml_orders WHERE paid = 1 AND created_at >= v_today_start) as today_orders,
            (SELECT COUNT(*) FROM public.ak_users WHERE created_at >= v_today_start) as today_new_users
    ),
    pending_tasks AS (
        SELECT 
            (SELECT COUNT(*) FROM public.ml_orders WHERE paid = 1 AND order_status = 1) as pending_delivery,
            (SELECT COUNT(*) FROM public.ml_product_skus WHERE stock <= 10) as stock_warning, -- 假设库存小于10为预警
            (SELECT COUNT(*) FROM public.ml_extract WHERE status = 0) as pending_extract
    )
    SELECT jsonb_build_object(
        'totals', (SELECT row_to_json(totals.*) FROM totals),
        'today', (SELECT row_to_json(today_stats.*) FROM today_stats),
        'pending', (SELECT row_to_json(pending_tasks.*) FROM pending_tasks)
    ) INTO v_stats;

    RETURN v_stats;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_overall_stats() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_overall_stats() TO authenticated;
-- =====================================================================================
-- Admin 系统维护 - 获取服务器环境信息 RPC
-- 位置docs/sql/30_rpc/admin/rpc_admin_get_system_info_v1.sql
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:获取服务器操作系统、数据库版本及运行环境信息
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_get_system_info()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_info JSONB;
    v_db_version TEXT;
BEGIN
    -- 1. 权限检查 (仅管理员)
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取数据库版本
    SELECT version() INTO v_db_version;

    -- 3. 构建返回信息
    v_info := jsonb_build_object(
        'server_os', 'Linux (Simulated)', -- 数据库侧通常难以直接获取完整的宿主系统信息
        'web_server', 'Nginx/1.24.0 (Simulated)',
        'db_engine', 'PostgreSQL',
        'db_version', v_db_version,
        'uts_runtime', 'uni-app x (UTS)',
        'auth_id', 'ZC2884891' -- 模拟授权码
    );

    RETURN v_info;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_system_info() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_system_info() TO authenticated;
-- =====================================================================================
-- Admin 系统功能 - 获取配置项 RPC
-- 位置docs/sql/30_rpc/admin/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_system_configs, ak_users 表已存在
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_system_config_get(
    p_key TEXT
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_value JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取配置值
    SELECT config_value INTO v_value
    FROM public.ml_system_configs
    WHERE config_key = p_key;

    RETURN v_value;
END;
$$;-- =====================================================================================
-- Admin 系统功能 - 保存/更新配置项 RPC
-- 位置docs/sql/30_rpc/admin/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_system_configs, ak_users 表已存在
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_system_config_save(
    p_key TEXT,
    p_value JSONB,
    p_description TEXT DEFAULT NULL
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 插入或更新配置
    INSERT INTO public.ml_system_configs (config_key, config_value, description, updated_at)
    VALUES (p_key, p_value, p_description, NOW())
    ON CONFLICT (config_key) DO UPDATE
    SET 
        config_value = EXCLUDED.config_value,
        description = COALESCE(EXCLUDED.description, public.ml_system_configs.description),
        updated_at = NOW();

    RETURN TRUE;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_system_config_save(TEXT, JSONB, TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_system_config_save(TEXT, JSONB, TEXT) TO authenticated;
-- =====================================================================================
-- RPC: rpc_analytics_user_gender_distribution
-- Version: v1
-- Purpose: 统计指定周期内新增用户的性别分布(用于 Admin/Analytics 图表)
-- Security: SECURITY DEFINER + 固定 search_path + 入口角色鉴权
-- Depends: public.ak_users, public.get_current_user_role()
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_analytics_user_gender_distribution(
  p_start_date DATE,
  p_end_date DATE
)
RETURNS TABLE (
  name TEXT,
  value BIGINT
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
  IF public.get_current_user_role() NOT IN ('admin', 'analytics') THEN
    RAISE EXCEPTION 'Permission denied: required role admin or analytics';
  END IF;

  RETURN QUERY
  SELECT
    CASE
      WHEN gender IS NULL OR TRIM(gender::text) = '' THEN '未知'
      WHEN LOWER(TRIM(gender::text)) = 'male' THEN '男'
      WHEN LOWER(TRIM(gender::text)) = 'female' THEN '女'
      WHEN LOWER(TRIM(gender::text)) = 'other' THEN '未知'
      ELSE '未知'
    END AS name,
    COUNT(*)::BIGINT AS value
  FROM public.ak_users
  WHERE created_at::DATE BETWEEN p_start_date AND p_end_date
  GROUP BY 1
  ORDER BY value DESC;
END;
$$;
-- =====================================================================================
-- 函数: check_admin_permission
-- 描述: 通用的 RBAC 权限校验函数
-- 参数: p_permission_code - 权限编码 (如 'role:delete', 'user:view')
-- 返回: BOOLEAN
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.check_admin_permission(
    p_permission_code TEXT DEFAULT NULL
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_user_id UUID;
    v_role TEXT;
BEGIN
    -- 1. 获取当前登录用户的 Profile ID 和角色
    SELECT id, role INTO v_user_id, v_role
    FROM public.ak_users
    WHERE auth_id = auth.uid();

    -- 2. 未登录或未找到 Profile
    IF v_user_id IS NULL THEN
        RETURN FALSE;
    END IF;

    -- 3. 超级管理员拥有所有权限 (保持向下兼容)
    IF v_role = 'admin' THEN
        RETURN TRUE;
    END IF;

    -- 4. 如果指定了权限编码,则检查 ak_permissions 体系
    IF p_permission_code IS NOT NULL THEN
        RETURN EXISTS (
            SELECT 1 
            FROM public.ak_admin_roles ar
            JOIN public.ak_role_permissions rp ON ar.role_id = rp.role_id
            JOIN public.ak_permissions p ON rp.permission_id = p.id
            WHERE ar.admin_id = v_user_id
              AND p.code = p_permission_code
              AND p.deleted_at IS NULL
              AND ar.deleted_at IS NULL
        );
    END IF;

    RETURN FALSE;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.check_admin_permission(TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.check_admin_permission(TEXT) TO authenticated;
-- =====================================================================================
-- RPC: get_current_user_role
-- Version: v1
-- Purpose: 获取当前登录用户的角色(用于 RPC 入口鉴权)
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends: public.ak_users (auth_id, role)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.get_current_user_role()
RETURNS TEXT
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
AS $$
  SELECT role
  FROM public.ak_users
  WHERE auth_id = auth.uid()
  LIMIT 1;
$$;-- =====================================================================================
-- Trigger Function: handle_new_user
-- Version: v2
-- Purpose: auth.users 新用户创建后,同步写入 public.ak_users权威用户表并保持 user_roles 兼容写入
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends:
--   - public.ak_users(auth_id,email,username,role)
--   - public.user_roles(user_id,role,created_by) (如存在)
-- Notes:
--   - 角色权威口径为 public.ak_users.role
--   - user_roles 为历史/兼容表:存在则写入,不存在则跳过
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  user_role TEXT := 'customer';
  user_email TEXT := NEW.email;
  user_name TEXT;
  has_user_roles BOOLEAN := FALSE;
BEGIN
  -- 1) 基于邮箱规则分配默认角色(可按需调整)
  IF user_email ILIKE '%@teacher.%' OR user_email ILIKE '%@edu.%' THEN
    user_role := 'teacher';
  ELSIF user_email ILIKE '%@admin.%' THEN
    user_role := 'admin';
  END IF;

  -- 2) 默认 username取邮箱 @ 前缀
  IF user_email IS NOT NULL AND POSITION('@' IN user_email) > 1 THEN
    user_name := SPLIT_PART(user_email, '@', 1);
  ELSE
    user_name := 'user';
  END IF;

  -- 3) 写入 ak_users权威
  -- 使用 ON CONFLICT 确保幂等:同一 auth_id 只会有一条记录
  INSERT INTO public.ak_users (auth_id, email, username, role)
  VALUES (NEW.id, user_email, user_name, user_role)
  ON CONFLICT (auth_id)
  DO UPDATE SET
    email = COALESCE(EXCLUDED.email, public.ak_users.email),
    username = COALESCE(EXCLUDED.username, public.ak_users.username),
    role = COALESCE(public.ak_users.role, EXCLUDED.role),
    updated_at = now();

  -- 4) 兼容写入 user_roles如果表存在
  SELECT EXISTS (
    SELECT 1
    FROM information_schema.tables
    WHERE table_schema='public'
      AND table_name='user_roles'
  ) INTO has_user_roles;

  IF has_user_roles THEN
    BEGIN
      INSERT INTO public.user_roles (user_id, role, created_by)
      VALUES (NEW.id, user_role, NEW.id);
    EXCEPTION WHEN unique_violation THEN
      -- 忽略重复
      NULL;
    END;
  END IF;

  -- 5) 更新 auth.users 元数据(可选保留)
  UPDATE auth.users
  SET raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) || jsonb_build_object('user_role', user_role)
  WHERE id = NEW.id;

  RETURN NEW;
END;
$$;
-- =====================================================================================
-- Trigger Function: handle_new_user
-- Version: v3
-- Purpose: auth.users 新用户创建后,同步写入 public.ak_users权威和 public.user_roles兼容--          此版本修复了向 user_roles 写入时可能因 role 为 NULL 导致的 NOT NULL 约束失败问题。
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends:
--   - public.ak_users(auth_id,email,username,role)
--   - public.user_roles(user_id,role)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  user_role TEXT;
  user_email TEXT := NEW.email;
  user_name TEXT;
  has_user_roles BOOLEAN := FALSE;
BEGIN
  -- 1) 基于邮箱规则分配默认角色(可按需调整)
  -- 确保 user_role 总有一个非 NULL 的值
  user_role := CASE
    WHEN user_email ILIKE '%@admin.%' THEN 'admin'
    WHEN user_email ILIKE '%@teacher.%' OR user_email ILIKE '%@edu.%' THEN 'teacher'
    ELSE 'consumer' -- 默认角色
  END;

  -- 2) 默认 username取邮箱 @ 前缀
  IF user_email IS NOT NULL AND POSITION('@' IN user_email) > 1 THEN
    user_name := SPLIT_PART(user_email, '@', 1);
  ELSE
    user_name := 'user_' || SUBSTRING(NEW.id::text, 1, 8); -- 使用 user_ + uid前8位作为备用名
  END IF;

  -- 3) 写入 ak_users权威
  -- 使用 ON CONFLICT 确保幂等:同一 auth_id 只会有一条记录
  INSERT INTO public.ak_users (auth_id, email, username, role)
  VALUES (NEW.id, user_email, user_name, user_role)
  ON CONFLICT (auth_id)
  DO UPDATE SET
    email = COALESCE(EXCLUDED.email, public.ak_users.email),
    username = COALESCE(EXCLUDED.username, public.ak_users.username),
    -- 只有当现有 role 为空时才更新,避免覆盖手动设置的 admin 角色
    role = COALESCE(public.ak_users.role, EXCLUDED.role),
    updated_at = now();

  -- 4) 兼容写入 user_roles如果表存在
  SELECT EXISTS (
    SELECT 1
    FROM information_schema.tables
    WHERE table_schema='public'
      AND table_name='user_roles'
  ) INTO has_user_roles;

  IF has_user_roles THEN
    BEGIN
      -- 确保插入的 role 不为 NULL即使上面的逻辑有误
      INSERT INTO public.user_roles (user_id, role, created_by)
      VALUES (NEW.id, COALESCE(user_role, 'customer'), NEW.id);
    EXCEPTION
      WHEN unique_violation THEN
        -- 忽略重复插入的错误
        NULL;
      WHEN not_null_violation THEN
        -- 记录非空约束错误,但不中断整个触发器
        RAISE NOTICE '[handle_new_user] WARNING: Failed to INSERT into user_roles due to NOT NULL violation. user_id: %, role: %', NEW.id, user_role;
    END;
  END IF;

  -- 5) 更新 auth.users 元数据(可选保留)
  UPDATE auth.users
  SET raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) || jsonb_build_object('user_role', user_role)
  WHERE id = NEW.id;

  RETURN NEW;
END;
$$;-- =====================================================================================
-- Trigger Function: handle_new_user
-- Version: v4
-- Purpose: auth.users 新用户创建后,优先读取 raw_user_meta_data.user_role 写入 ak_users.role。
--          解决 delivery / merchant 注册时被错误降级为默认 consumer 的问题。
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends:
--   - public.ak_users(auth_id,email,username,role)
--   - public.user_roles(user_id,role)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  user_role TEXT;
  metadata_role TEXT;
  user_email TEXT := NEW.email;
  user_name TEXT;
  ak_user_id UUID;
  has_user_roles BOOLEAN := FALSE;
  has_delivery_staff BOOLEAN := FALSE;
BEGIN
  metadata_role := NULLIF(TRIM(COALESCE(NEW.raw_user_meta_data ->> 'user_role', '')), '');

  user_role := CASE
    WHEN metadata_role IN ('customer', 'merchant', 'delivery', 'service', 'admin') THEN metadata_role
    WHEN user_email ILIKE '%@admin.%' THEN 'admin'
    WHEN user_email ILIKE '%@teacher.%' OR user_email ILIKE '%@edu.%' THEN 'customer'
    ELSE 'customer'
  END;

  IF user_email IS NOT NULL AND POSITION('@' IN user_email) > 1 THEN
    user_name := SPLIT_PART(user_email, '@', 1);
  ELSE
    user_name := 'user_' || SUBSTRING(NEW.id::text, 1, 8);
  END IF;

  INSERT INTO public.ak_users (auth_id, email, username, role)
  VALUES (NEW.id, user_email, user_name, user_role)
  ON CONFLICT (auth_id)
  DO UPDATE SET
    email = COALESCE(EXCLUDED.email, public.ak_users.email),
    username = COALESCE(EXCLUDED.username, public.ak_users.username),
    role = COALESCE(NULLIF(public.ak_users.role, ''), EXCLUDED.role),
    updated_at = now()
  RETURNING id INTO ak_user_id;

  IF user_role = 'delivery' THEN
    SELECT EXISTS (
      SELECT 1
      FROM information_schema.tables
      WHERE table_schema = 'public'
        AND table_name = 'ml_delivery_staff'
    ) INTO has_delivery_staff;

    IF has_delivery_staff THEN
      INSERT INTO public.ml_delivery_staff (
        uid,
        nickname,
        phone,
        status,
        is_active
      )
      SELECT
        ak_user_id,
        user_name,
        '',
        1,
        TRUE
      WHERE NOT EXISTS (
        SELECT 1
        FROM public.ml_delivery_staff
        WHERE uid = ak_user_id
      );
    END IF;
  END IF;

  SELECT EXISTS (
    SELECT 1
    FROM information_schema.tables
    WHERE table_schema = 'public'
      AND table_name = 'user_roles'
  ) INTO has_user_roles;

  IF has_user_roles THEN
    BEGIN
      INSERT INTO public.user_roles (user_id, role, created_by)
      VALUES (NEW.id, user_role, NEW.id)
      ON CONFLICT DO NOTHING;
    EXCEPTION
      WHEN check_violation THEN
        RAISE NOTICE '[handle_new_user_v4] WARNING: Skipped user_roles insert due to check violation. user_id: %, role: %', NEW.id, user_role;
      WHEN not_null_violation THEN
        RAISE NOTICE '[handle_new_user_v4] WARNING: Failed to INSERT into user_roles due to NOT NULL violation. user_id: %, role: %', NEW.id, user_role;
      WHEN others THEN
        RAISE NOTICE '[handle_new_user_v4] WARNING: Skipped user_roles insert due to unexpected error. user_id: %, role: %, err: %', NEW.id, user_role, SQLERRM;
    END;
  END IF;

  UPDATE auth.users
  SET raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) || jsonb_build_object('user_role', user_role)
  WHERE id = NEW.id;

  RETURN NEW;
END;
$$;-- RPC: rpc_admin_delete_permission
-- 管理端删除功能权限/菜单(支持级联软删除关联的角色权限映射)

CREATE OR REPLACE FUNCTION public.rpc_admin_delete_permission(
    p_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查 (使用通用权限校验函数,权限编码: permission:delete)
    IF NOT public.check_admin_permission('permission:delete') THEN
        RAISE EXCEPTION 'Permission denied: permission:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id
    FROM public.ak_users
    WHERE auth_id = auth.uid();

    -- 3. 级联软删除:先删除所有关联了该权限的角色映射
    UPDATE public.ak_role_permissions
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE permission_id = p_id AND deleted_at IS NULL;

    -- 4. 最后软删除权限本身
    UPDATE public.ak_permissions 
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_permission(UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_permission(UUID) TO authenticated;
-- RPC: rpc_admin_delete_role
-- 管理端删除角色(支持级联软删除关联权限)

CREATE OR REPLACE FUNCTION public.rpc_admin_delete_role(
    p_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查 (使用通用权限校验函数,权限编码: role:delete)
    IF NOT public.check_admin_permission('role:delete') THEN
        RAISE EXCEPTION 'Permission denied: role:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id
    FROM public.ak_users
    WHERE auth_id = auth.uid();

    -- 3. 级联软删除:先删除该角色下的所有权限关联
    UPDATE public.ak_role_permissions
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE role_id = p_id AND deleted_at IS NULL;

    -- 4. 级联软删除:再删除该角色下的所有管理员关联
    UPDATE public.ak_admin_roles
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE role_id = p_id AND deleted_at IS NULL;

    -- 5. 最后软删除角色本身
    UPDATE public.ak_roles
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_role(UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_role(UUID) TO authenticated;
-- RPC: rpc_admin_get_admin_list
-- 管理端获取管理员列表
-- 筛选 ak_users 表中 role 为 'admin' 或 'analytics' 的用户,并关联显示其角色信息

CREATE OR REPLACE FUNCTION public.rpc_admin_get_admin_list(
    p_search TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT NULL,
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_offset INTEGER := (p_page - 1) * p_page_size;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查 (仅管理员)
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ak_users u
    WHERE u.role IN ('admin', 'analytics')
      AND (p_status IS NULL OR u.is_active = (p_status = 1))
      AND (p_search IS NULL OR u.username ILIKE '%' || p_search || '%' OR u.real_name ILIKE '%' || p_search || '%');

    -- 3. 获取数据列表 (关联角色)
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            u.id,
            u.username,
            u.real_name,
            u.role,
            u.is_active,
            u.last_login_at,
            u.last_login_ip,
            (
                SELECT jsonb_agg(r.name)
                FROM public.ak_admin_roles ar
                JOIN public.ak_roles r ON r.id = ar.role_id
                WHERE ar.user_id = u.id
            ) as roles
        FROM public.ak_users u
        WHERE u.role IN ('admin', 'analytics')
          AND (p_status IS NULL OR u.is_active = (p_status = 1))
          AND (p_search IS NULL OR u.username ILIKE '%' || p_search || '%' OR u.real_name ILIKE '%' || p_search || '%')
        ORDER BY u.created_at DESC
        LIMIT p_page_size OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_admin_list(TEXT, SMALLINT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_admin_list(TEXT, SMALLINT, INTEGER, INTEGER) TO authenticated;
-- RPC: rpc_admin_get_permission_list
-- 管理端获取全量权限/菜单列表 (供前端构建树形结构)

CREATE OR REPLACE FUNCTION public.rpc_admin_get_permission_list()
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取全量数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            id, parent_id, name, code, type, 
            path, icon, sort_order, is_visible,
            created_at, updated_at
        FROM public.ak_permissions
        WHERE deleted_at IS NULL
        ORDER BY sort_order ASC, created_at ASC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_permission_list() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_permission_list() TO authenticated;
-- RPC: rpc_admin_get_role_list
-- 管理端获取角色分页列表

CREATE OR REPLACE FUNCTION public.rpc_admin_get_role_list(
    p_search TEXT DEFAULT NULL,
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_offset INTEGER := (p_page - 1) * p_page_size;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ak_roles
    WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%' OR code ILIKE '%' || p_search || '%');

    -- 3. 获取明细
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            id, name, code, description, is_active, 
            created_at, updated_at
        FROM public.ak_roles
        WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%' OR code ILIKE '%' || p_search || '%')
        ORDER BY created_at DESC
        LIMIT p_page_size OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_role_list(TEXT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_role_list(TEXT, INTEGER, INTEGER) TO authenticated;
-- RPC: rpc_admin_save_permission
-- 管理端新增或更新功能权限/菜单

CREATE OR REPLACE FUNCTION public.rpc_admin_save_permission(
    p_id UUID DEFAULT NULL,
    p_parent_id UUID DEFAULT NULL,
    p_name TEXT DEFAULT NULL,
    p_code TEXT DEFAULT NULL,
    p_type TEXT DEFAULT 'menu',
    p_path TEXT DEFAULT NULL,
    p_icon TEXT DEFAULT NULL,
    p_sort_order INTEGER DEFAULT 0,
    p_is_visible BOOLEAN DEFAULT TRUE
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 新增
    IF p_id IS NULL THEN
        IF p_name IS NULL OR p_code IS NULL THEN
            RAISE EXCEPTION 'Missing required fields: name or code';
        END IF;

        INSERT INTO public.ak_permissions (
            parent_id, name, code, type, path, icon, sort_order, is_visible
        ) VALUES (
            p_parent_id, p_name, p_code, p_type, p_path, p_icon, p_sort_order, p_is_visible
        ) RETURNING id INTO v_id;
    ELSE
        -- 3. 更新
        UPDATE public.ak_permissions
        SET 
            parent_id = COALESCE(p_parent_id, parent_id),
            name = COALESCE(p_name, name),
            code = COALESCE(p_code, code),
            type = COALESCE(p_type, type),
            path = COALESCE(p_path, path),
            icon = COALESCE(p_icon, icon),
            sort_order = COALESCE(p_sort_order, sort_order),
            is_visible = COALESCE(p_is_visible, is_visible),
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Permission item not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_permission(UUID, UUID, TEXT, TEXT, TEXT, TEXT, TEXT, INTEGER, BOOLEAN) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_permission(UUID, UUID, TEXT, TEXT, TEXT, TEXT, TEXT, INTEGER, BOOLEAN) TO authenticated;
-- RPC: rpc_admin_save_role
-- 管理端新增或更新角色

CREATE OR REPLACE FUNCTION public.rpc_admin_save_role(
    p_id UUID DEFAULT NULL,
    p_name TEXT DEFAULT NULL,
    p_code TEXT DEFAULT NULL,
    p_description TEXT DEFAULT NULL,
    p_is_active BOOLEAN DEFAULT TRUE
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 新增
    IF p_id IS NULL THEN
        IF p_name IS NULL OR p_code IS NULL THEN
            RAISE EXCEPTION 'Missing required fields: name or code';
        END IF;

        INSERT INTO public.ak_roles (
            name, code, description, is_active
        ) VALUES (
            p_name, p_code, p_description, p_is_active
        ) RETURNING id INTO v_id;
    ELSE
        -- 3. 更新
        UPDATE public.ak_roles
        SET 
            name = COALESCE(p_name, name),
            code = COALESCE(p_code, code),
            description = COALESCE(p_description, description),
            is_active = COALESCE(p_is_active, is_active),
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Role not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_role(UUID, TEXT, TEXT, TEXT, BOOLEAN) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_role(UUID, TEXT, TEXT, TEXT, BOOLEAN) TO authenticated;
-- =====================================================================================
-- RPC: rpc_admin_article_category_delete
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除文章分类(支持级联软删除分类下的文章)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('cms:category:delete') THEN
        RAISE EXCEPTION 'Permission denied: cms:category:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 级联软删除:该分类下的所有文章
    UPDATE public.ml_articles
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE category_id = p_id AND deleted_at IS NULL;

    -- 4. 软删除分类本身
    UPDATE public.ml_article_categories
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_article_category_delete IS '管理员删除文章分类(级联软删除关联文章)';
-- =====================================================================================
-- RPC: rpc_admin_article_category_list
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取文章分类列表
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_article_categories
    WHERE (p_search IS NULL OR name ILIKE '%' || p_search || '%');

    -- 3. 获取列表
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT id, name, icon, sort, status, created_at, updated_at
        FROM public.ml_article_categories
        WHERE (p_search IS NULL OR name ILIKE '%' || p_search || '%')
        ORDER BY sort ASC, created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_article_category_save
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新文章分类
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_save(
    p_id UUID DEFAULT NULL,
    p_name TEXT DEFAULT NULL,
    p_icon TEXT DEFAULT NULL,
    p_sort INTEGER DEFAULT 0,
    p_status SMALLINT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 参数校验
    IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
        RAISE EXCEPTION 'Invalid name';
    END IF;

    -- 3. 新增
    IF p_id IS NULL THEN
        INSERT INTO public.ml_article_categories (
            name, icon, sort, status
        ) VALUES (
            p_name, p_icon, p_sort, p_status
        ) RETURNING id INTO v_id;
    ELSE
        -- 4. 更新
        UPDATE public.ml_article_categories
        SET 
            name = p_name,
            icon = COALESCE(p_icon, icon),
            sort = p_sort,
            status = p_status,
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Category not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_article_category_save IS '管理员新增或更新文章分类';
-- =====================================================================================
-- RPC: rpc_admin_article_category_set_status
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端切换文章分类启用/禁用状态
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_set_status(
    p_id UUID,
    p_status SMALLINT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 更新状态
    UPDATE public.ml_article_categories
    SET status = p_status,
        updated_at = now()
    WHERE id = p_id;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_article_category_set_status IS '管理员设置文章分类状态';
-- =====================================================================================
-- RPC: rpc_admin_article_delete
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除文章记录(使用通用权限校验)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('cms:article:delete') THEN
        RAISE EXCEPTION 'Permission denied: cms:article:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 执行软删除
    UPDATE public.ml_articles
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_article_delete IS '管理员删除文章记录';
-- =====================================================================================
-- RPC: rpc_admin_article_get_detail
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端获取指定文章的完整详情
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_get_detail(
    p_id UUID
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_item JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取详情
    SELECT jsonb_build_object(
        'id', a.id,
        'category_id', a.category_id,
        'category_name', c.name,
        'title', a.title,
        'author', a.author,
        'image', a.image,
        'description', a.description,
        'content', a.content,
        'status', a.status,
        'views', a.views,
        'is_banner', a.is_banner,
        'is_hot', a.is_hot,
        'linked_product_id', a.linked_product_id,
        'created_at', a.created_at,
        'updated_at', a.updated_at
    ) INTO v_item
    FROM public.ml_articles a
    LEFT JOIN public.ml_article_categories c ON c.id = a.category_id
    WHERE a.id = p_id;

    IF v_item IS NULL THEN
        RAISE EXCEPTION 'Article not found';
    END IF;

    RETURN v_item;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_article_get_detail IS '管理员获取文章完整详情';
-- =====================================================================================
-- RPC: rpc_admin_article_list
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取文章列表,支持搜索、分类筛选及状态过滤
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_category_id UUID DEFAULT NULL,
    p_status SMALLINT DEFAULT NULL,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_articles a
    WHERE (p_category_id IS NULL OR a.category_id = p_category_id)
      AND (p_status IS NULL OR a.status = p_status)
      AND (p_search IS NULL OR a.title ILIKE '%' || p_search || '%' OR a.author ILIKE '%' || p_search || '%');

    -- 3. 获取列表数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            a.id,
            a.category_id,
            c.name as category_name,
            a.title,
            a.author,
            a.image,
            a.description,
            a.status,
            a.views,
            a.is_banner,
            a.is_hot,
            a.created_at,
            a.updated_at
        FROM public.ml_articles a
        LEFT JOIN public.ml_article_categories c ON c.id = a.category_id
        WHERE (p_category_id IS NULL OR a.category_id = p_category_id)
          AND (p_status IS NULL OR a.status = p_status)
          AND (p_search IS NULL OR a.title ILIKE '%' || p_search || '%' OR a.author ILIKE '%' || p_search || '%')
        ORDER BY a.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_article_list IS '管理员分页查询文章列表';
-- =====================================================================================
-- RPC: rpc_admin_article_save
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新文章内容
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_save(
    p_id UUID DEFAULT NULL,
    p_category_id UUID DEFAULT NULL,
    p_title TEXT DEFAULT NULL,
    p_author TEXT DEFAULT NULL,
    p_image TEXT DEFAULT NULL,
    p_description TEXT DEFAULT NULL,
    p_content TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT 0,
    p_is_banner BOOLEAN DEFAULT FALSE,
    p_is_hot BOOLEAN DEFAULT FALSE,
    p_linked_product_id UUID DEFAULT NULL
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 参数校验
    IF p_title IS NULL OR length(trim(p_title)) = 0 THEN
        RAISE EXCEPTION 'Invalid title';
    END IF;
    IF p_category_id IS NULL THEN
        RAISE EXCEPTION 'Category is required';
    END IF;

    -- 3. 新增
    IF p_id IS NULL THEN
        INSERT INTO public.ml_articles (
            category_id, title, author, image, description, content, 
            status, is_banner, is_hot, linked_product_id
        ) VALUES (
            p_category_id, p_title, p_author, p_image, p_description, p_content,
            p_status, p_is_banner, p_is_hot, p_linked_product_id
        ) RETURNING id INTO v_id;
    ELSE
        -- 4. 更新
        UPDATE public.ml_articles
        SET 
            category_id = COALESCE(p_category_id, category_id),
            title = COALESCE(p_title, title),
            author = COALESCE(p_author, author),
            image = COALESCE(p_image, image),
            description = COALESCE(p_description, description),
            content = COALESCE(p_content, content),
            status = COALESCE(p_status, status),
            is_banner = COALESCE(p_is_banner, is_banner),
            is_hot = COALESCE(p_is_hot, is_hot),
            linked_product_id = p_linked_product_id,
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Article not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_article_save IS '管理员新增或更新文章内容';
-- =====================================================================================
-- RPC: rpc_admin_article_set_status
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端切换文章发布/下架状态
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_article_set_status(
    p_id UUID,
    p_status SMALLINT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 更新状态
    UPDATE public.ml_articles
    SET status = p_status,
        updated_at = now()
    WHERE id = p_id;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_article_set_status IS '管理员设置文章发布状态';
-- RPC: rpc_admin_delete_diy_page
-- 管理端删除 DIY 页面配置(支持权限检查与首页保护)

CREATE OR REPLACE FUNCTION public.rpc_admin_delete_diy_page(
  p_id uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_ok boolean;
  v_user_id UUID;
BEGIN
  -- 1. 权限检查 (使用通用权限校验函数)
  IF NOT public.check_admin_permission('decoration:page:delete') THEN
    RAISE EXCEPTION 'Permission denied: decoration:page:delete';
  END IF;

  -- 2. 获取当前操作用户 ID
  SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

  -- 3. 校验:不允许删除当前生效的首页
  IF EXISTS (
    SELECT 1 FROM public.ak_diy_pages 
    WHERE id = p_id AND is_home = true AND deleted_at IS NULL
  ) THEN
    RAISE EXCEPTION 'cannot delete the active home page';
  END IF;

  -- 4. 执行软删除:标记 deleted_at
  UPDATE public.ak_diy_pages
  SET deleted_at = now(),
      deleted_by = v_user_id
  WHERE id = p_id AND deleted_at IS NULL;
  
  GET DIAGNOSTICS v_ok = ROW_COUNT;
  RETURN v_ok;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_diy_page(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_diy_page(uuid) TO authenticated;
-- RPC: rpc_admin_get_diy_page_list
-- 管理端获取 DIY 页面分页列表
-- 支持按名称搜索和按类型筛选

CREATE OR REPLACE FUNCTION public.rpc_admin_get_diy_page_list(
  p_search text DEFAULT NULL,
  p_type text DEFAULT NULL,
  p_page integer DEFAULT 1,
  p_page_size integer DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_page integer := GREATEST(1, COALESCE(p_page, 1));
  v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
  v_offset integer := (v_page - 1) * v_page_size;
  v_total bigint;
  v_items jsonb;
BEGIN
  -- 1. 权限检查 (仅管理员或分析员)
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  -- 2. 获取总数
  SELECT COUNT(*) INTO v_total
  FROM public.ak_diy_pages
  WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%')
    AND (p_type IS NULL OR type = p_type);

  -- 3. 获取明细
  SELECT jsonb_agg(t) INTO v_items
  FROM (
    SELECT 
      id, name, type, is_home, is_active, 
      created_at, updated_at
    FROM public.ak_diy_pages
    WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%')
      AND (p_type IS NULL OR type = p_type)
    ORDER BY created_at DESC
    LIMIT v_page_size OFFSET v_offset
  ) t;

  -- 4. 返回 JSON 结果
  RETURN jsonb_build_object(
    'total', v_total,
    'items', COALESCE(v_items, '[]'::jsonb)
  );
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_diy_page_list(text, text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_diy_page_list(text, text, integer, integer) TO authenticated;
-- RPC: rpc_admin_save_diy_page
-- 管理端新增或更新 DIY 页面配置

CREATE OR REPLACE FUNCTION public.rpc_admin_save_diy_page(
  p_id uuid DEFAULT NULL,
  p_name text DEFAULT NULL,
  p_type text DEFAULT NULL,
  p_config jsonb DEFAULT '{}'::jsonb,
  p_is_active boolean DEFAULT true
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_id uuid;
BEGIN
  -- 1. 权限检查 (仅管理员)
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role = 'admin'
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  -- 2. 新增或更新
  IF p_id IS NULL THEN
    INSERT INTO public.ak_diy_pages (
      name, type, config, is_active, updated_by, created_by
    ) VALUES (
      p_name, p_type, p_config, p_is_active, auth.uid(), auth.uid()
    ) RETURNING id INTO v_id;
  ELSE
    UPDATE public.ak_diy_pages
    SET
      name = COALESCE(p_name, name),
      type = COALESCE(p_type, type),
      config = COALESCE(p_config, config),
      is_active = COALESCE(p_is_active, is_active),
      updated_at = now(),
      updated_by = auth.uid()
    WHERE id = p_id
    RETURNING id INTO v_id;

    IF v_id IS NULL THEN
      RAISE EXCEPTION 'page not found';
    END IF;
  END IF;

  RETURN v_id;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_diy_page(uuid, text, text, jsonb, boolean) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_diy_page(uuid, text, text, jsonb, boolean) TO authenticated;
-- RPC: rpc_admin_set_home_page
-- 管理端设置生效首页
-- 逻辑:先取消所有同类型页面的 is_home 状态,再设置目标页面为 is_home

CREATE OR REPLACE FUNCTION public.rpc_admin_set_home_page(
  p_id uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_type text;
BEGIN
  -- 1. 权限检查 (仅管理员)
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role = 'admin'
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  -- 2. 获取目标页面类型
  SELECT type INTO v_type FROM public.ak_diy_pages WHERE id = p_id;
  IF v_type IS NULL THEN
    RAISE EXCEPTION 'page not found';
  END IF;

  -- 3. 原子切换:同一类型的页面只能有一个 is_home
  UPDATE public.ak_diy_pages SET is_home = false WHERE type = v_type;
  UPDATE public.ak_diy_pages SET is_home = true WHERE id = p_id;

  RETURN true;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_set_home_page(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_set_home_page(uuid) TO authenticated;
-- RPC: rpc_admin_delete_delivery_staff
-- 管理端删除配送员(支持权限检查)

CREATE OR REPLACE FUNCTION public.rpc_admin_delete_delivery_staff(
    p_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查 (使用通用权限校验函数)
    IF NOT public.check_admin_permission('delivery:staff:delete') THEN
        RAISE EXCEPTION 'Permission denied: delivery:staff:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 执行软删除:标记 deleted_at
    UPDATE public.ml_delivery_staff
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_delivery_staff(UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_delivery_staff(UUID) TO authenticated;
-- RPC: rpc_admin_delete_delivery_station
-- 管理端删除提货点/核销点(支持级联软删除配送员关联)

CREATE OR REPLACE FUNCTION public.rpc_admin_delete_delivery_station(
    p_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('delivery:station:delete') THEN
        RAISE EXCEPTION 'Permission denied: delivery:station:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 级联软删除:该站点下的所有配送员
    UPDATE public.ml_delivery_staff
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE station_id = p_id AND deleted_at IS NULL;

    -- 4. 执行软删除站点本身
    UPDATE public.ml_delivery_stations
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_delivery_station(UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_delivery_station(UUID) TO authenticated;
-- RPC: rpc_admin_get_delivery_staff_list
-- 管理端获取配送员分页列表
-- 支持按姓名或手机号搜索

CREATE OR REPLACE FUNCTION public.rpc_admin_get_delivery_staff_list(
    p_search TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT NULL,
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_offset INTEGER := (p_page - 1) * p_page_size;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查 (仅管理员)
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_delivery_staff
    WHERE (p_status IS NULL OR status = p_status)
      AND (p_search IS NULL OR p_search = '' OR nickname ILIKE '%' || p_search || '%' OR phone ILIKE '%' || p_search || '%');

    -- 3. 获取明细
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            id, uid, nickname, avatar, phone, status, is_active,
            created_at, updated_at
        FROM public.ml_delivery_staff
        WHERE (p_status IS NULL OR status = p_status)
          AND (p_search IS NULL OR p_search = '' OR nickname ILIKE '%' || p_search || '%' OR phone ILIKE '%' || p_search || '%')
        ORDER BY created_at DESC
        LIMIT p_page_size OFFSET v_offset
    ) t;

    -- 4. 返回结果
    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER) TO authenticated;
-- RPC: rpc_admin_get_delivery_staff_list
-- 管理端获取服务人员分页列表v2

DROP FUNCTION IF EXISTS public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER);

CREATE OR REPLACE FUNCTION public.rpc_admin_get_delivery_staff_list(
    p_search TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT NULL,
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_offset INTEGER := (p_page - 1) * p_page_size;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    SELECT COUNT(*) INTO v_total
    FROM public.ml_delivery_staff s
    WHERE s.deleted_at IS NULL
      AND (p_status IS NULL OR s.status = p_status)
      AND (
        p_search IS NULL OR p_search = ''
        OR s.nickname ILIKE '%' || p_search || '%'
        OR s.phone ILIKE '%' || p_search || '%'
        OR s.staff_no ILIKE '%' || p_search || '%'
      );

    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT
            s.id,
            s.uid,
            s.station_id,
            st.name AS station_name,
            s.staff_no,
            s.nickname,
            s.avatar,
            s.phone,
            s.status,
            s.is_active,
            s.online_status,
            s.certificate_status,
            s.certificate_expire_at,
            s.service_area,
            s.skills,
            s.created_at,
            s.updated_at
        FROM public.ml_delivery_staff s
        LEFT JOIN public.ml_delivery_stations st ON st.id = s.station_id AND st.deleted_at IS NULL
        WHERE s.deleted_at IS NULL
          AND (p_status IS NULL OR s.status = p_status)
          AND (
            p_search IS NULL OR p_search = ''
            OR s.nickname ILIKE '%' || p_search || '%'
            OR s.phone ILIKE '%' || p_search || '%'
            OR s.staff_no ILIKE '%' || p_search || '%'
          )
        ORDER BY s.created_at DESC
        LIMIT p_page_size OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER) TO authenticated;-- RPC: rpc_admin_get_delivery_station_list
-- 管理端获取提货点/核销点分页列表
-- 支持按名称、地址或手机号搜索

CREATE OR REPLACE FUNCTION public.rpc_admin_get_delivery_station_list(
    p_search TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT NULL,
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_offset INTEGER := (p_page - 1) * p_page_size;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查 (仅管理员)
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_delivery_stations
    WHERE (p_status IS NULL OR status = p_status)
      AND (p_search IS NULL OR p_search = '' 
           OR name ILIKE '%' || p_search || '%' 
           OR address ILIKE '%' || p_search || '%' 
           OR phone ILIKE '%' || p_search || '%');

    -- 3. 获取明细
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            id, name, phone, address, image, 
            lng, lat, status, sort_order, business_hours,
            created_at, updated_at
        FROM public.ml_delivery_stations
        WHERE (p_status IS NULL OR status = p_status)
          AND (p_search IS NULL OR p_search = '' 
               OR name ILIKE '%' || p_search || '%' 
               OR address ILIKE '%' || p_search || '%' 
               OR phone ILIKE '%' || p_search || '%')
        ORDER BY sort_order ASC, created_at DESC
        LIMIT p_page_size OFFSET v_offset
    ) t;

    -- 4. 返回结果
    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_delivery_station_list(TEXT, SMALLINT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_delivery_station_list(TEXT, SMALLINT, INTEGER, INTEGER) TO authenticated;
-- RPC: rpc_admin_save_delivery_staff
-- 管理端新增或更新配送员信息

CREATE OR REPLACE FUNCTION public.rpc_admin_save_delivery_staff(
    p_id UUID DEFAULT NULL,
    p_nickname TEXT DEFAULT NULL,
    p_avatar TEXT DEFAULT NULL,
    p_phone TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT 1
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查 (仅管理员)
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 参数校验
    IF p_nickname IS NULL OR p_phone IS NULL THEN
        RAISE EXCEPTION 'Missing required fields: nickname or phone';
    END IF;

    -- 3. 新增或更新
    IF p_id IS NULL THEN
        INSERT INTO public.ml_delivery_staff (
            nickname, avatar, phone, status
        ) VALUES (
            p_nickname, p_avatar, p_phone, p_status
        ) RETURNING id INTO v_id;
    ELSE
        UPDATE public.ml_delivery_staff
        SET 
            nickname = COALESCE(p_nickname, nickname),
            avatar = COALESCE(p_avatar, avatar),
            phone = COALESCE(p_phone, phone),
            status = COALESCE(p_status, status),
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Delivery staff not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_delivery_staff(UUID, TEXT, TEXT, TEXT, SMALLINT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_delivery_staff(UUID, TEXT, TEXT, TEXT, SMALLINT) TO authenticated;
-- RPC: rpc_admin_save_delivery_staff
-- 管理端新增或更新服务人员信息v2

DROP FUNCTION IF EXISTS public.rpc_admin_save_delivery_staff(UUID, TEXT, TEXT, TEXT, SMALLINT);

CREATE OR REPLACE FUNCTION public.rpc_admin_save_delivery_staff(
    p_id UUID DEFAULT NULL,
    p_uid UUID DEFAULT NULL,
    p_station_id UUID DEFAULT NULL,
    p_staff_no TEXT DEFAULT NULL,
    p_nickname TEXT DEFAULT NULL,
    p_avatar TEXT DEFAULT NULL,
    p_phone TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT 1,
    p_online_status TEXT DEFAULT 'resting',
    p_certificate_status TEXT DEFAULT 'pending',
    p_certificate_expire_at DATE DEFAULT NULL,
    p_service_area TEXT DEFAULT '',
    p_skills JSONB DEFAULT '[]'::jsonb
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_id UUID;
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    IF p_nickname IS NULL OR p_phone IS NULL THEN
        RAISE EXCEPTION 'Missing required fields: nickname or phone';
    END IF;

    IF p_online_status NOT IN ('online', 'resting', 'busy') THEN
        RAISE EXCEPTION 'Invalid online_status';
    END IF;

    IF p_certificate_status NOT IN ('valid', 'expired', 'pending') THEN
        RAISE EXCEPTION 'Invalid certificate_status';
    END IF;

    IF p_station_id IS NOT NULL AND NOT EXISTS (
        SELECT 1
        FROM public.ml_delivery_stations s
        WHERE s.id = p_station_id AND s.deleted_at IS NULL
    ) THEN
        RAISE EXCEPTION 'Delivery station not found';
    END IF;

    IF p_id IS NULL THEN
        INSERT INTO public.ml_delivery_staff (
            uid, station_id, staff_no, nickname, avatar, phone, status,
            online_status, certificate_status, certificate_expire_at,
            service_area, skills
        ) VALUES (
            p_uid, p_station_id, NULLIF(p_staff_no, ''), p_nickname, p_avatar, p_phone, p_status,
            p_online_status, p_certificate_status, p_certificate_expire_at,
            COALESCE(p_service_area, ''), COALESCE(p_skills, '[]'::jsonb)
        ) RETURNING id INTO v_id;
    ELSE
        UPDATE public.ml_delivery_staff
        SET
            uid = COALESCE(p_uid, uid),
            station_id = p_station_id,
            staff_no = CASE WHEN p_staff_no IS NULL OR p_staff_no = '' THEN NULL ELSE p_staff_no END,
            nickname = COALESCE(p_nickname, nickname),
            avatar = COALESCE(p_avatar, avatar),
            phone = COALESCE(p_phone, phone),
            status = COALESCE(p_status, status),
            online_status = COALESCE(p_online_status, online_status),
            certificate_status = COALESCE(p_certificate_status, certificate_status),
            certificate_expire_at = p_certificate_expire_at,
            service_area = COALESCE(p_service_area, service_area),
            skills = COALESCE(p_skills, skills),
            updated_at = now()
        WHERE id = p_id AND deleted_at IS NULL
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Delivery staff not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_save_delivery_staff(UUID, UUID, UUID, TEXT, TEXT, TEXT, TEXT, SMALLINT, TEXT, TEXT, DATE, TEXT, JSONB) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_delivery_staff(UUID, UUID, UUID, TEXT, TEXT, TEXT, TEXT, SMALLINT, TEXT, TEXT, DATE, TEXT, JSONB) TO authenticated;-- RPC: rpc_admin_save_delivery_station
-- 管理端新增或更新提货点/核销点信息

CREATE OR REPLACE FUNCTION public.rpc_admin_save_delivery_station(
    p_id UUID DEFAULT NULL,
    p_name TEXT DEFAULT NULL,
    p_phone TEXT DEFAULT NULL,
    p_address TEXT DEFAULT NULL,
    p_image TEXT DEFAULT NULL,
    p_lng NUMERIC DEFAULT NULL,
    p_lat NUMERIC DEFAULT NULL,
    p_status SMALLINT DEFAULT 1,
    p_sort_order INTEGER DEFAULT 0,
    p_business_hours JSONB DEFAULT NULL
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查 (仅管理员)
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 参数校验
    IF p_name IS NULL OR p_phone IS NULL OR p_address IS NULL THEN
        RAISE EXCEPTION 'Missing required fields: name, phone or address';
    END IF;

    -- 3. 新增或更新
    IF p_id IS NULL THEN
        INSERT INTO public.ml_delivery_stations (
            name, phone, address, image, lng, lat, status, sort_order, business_hours
        ) VALUES (
            p_name, p_phone, p_address, p_image, p_lng, p_lat, p_status, p_sort_order, p_business_hours
        ) RETURNING id INTO v_id;
    ELSE
        UPDATE public.ml_delivery_stations
        SET 
            name = COALESCE(p_name, name),
            phone = COALESCE(p_phone, phone),
            address = COALESCE(p_address, address),
            image = COALESCE(p_image, image),
            lng = COALESCE(p_lng, lng),
            lat = COALESCE(p_lat, lat),
            status = COALESCE(p_status, status),
            sort_order = COALESCE(p_sort_order, sort_order),
            business_hours = COALESCE(p_business_hours, business_hours),
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Station not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_delivery_station(UUID, TEXT, TEXT, TEXT, TEXT, NUMERIC, NUMERIC, SMALLINT, INTEGER, JSONB) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_delivery_station(UUID, TEXT, TEXT, TEXT, TEXT, NUMERIC, NUMERIC, SMALLINT, INTEGER, JSONB) TO authenticated;
-- =====================================================================================
-- RPC: rpc_delivery_* homecare actions
-- Version: v1
-- Purpose: 为 delivery 页面当前 api/delivery.uts 已固定的 rpc_delivery_* 契约补齐 SQL 端实现。
-- Notes:
-- 1. 优先兼容 ec/hc 新链,检测不到表或执行失败时回退 hss 旧链。
-- 2. 返回 JSON 结构直接对齐 delivery/types/delivery.uts。
-- 3. message_list 与 record_list 采用最小可用实现,避免前端继续落入 fallback。
-- =====================================================================================

-- 迁移正文见同批文件mall_sql/migrations/20260526_delivery_homecare_rpc_v1.sql-- =====================================================================================
-- RPC: rpc_homecare_dispatch_candidate
-- Version: v1
-- Purpose: 为居家上门服务返回单个可派单候选人,替代公开可派单人员 RLS。
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends: public.ak_users, public.ml_delivery_staff
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_homecare_dispatch_candidate(
    p_service_code TEXT DEFAULT NULL,
    p_station_id UUID DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_candidate JSONB;
BEGIN
    IF auth.uid() IS NULL OR NOT EXISTS (
        SELECT 1
        FROM public.ak_users u
        WHERE u.auth_id = auth.uid()
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    SELECT jsonb_build_object(
        'id', s.id,
        'uid', s.uid,
        'station_id', s.station_id,
        'status', s.status,
        'online_status', s.online_status,
        'updated_at', s.updated_at,
        'created_at', s.created_at
    )
    INTO v_candidate
    FROM public.ml_delivery_staff s
    WHERE s.deleted_at IS NULL
      AND s.status = 1
      AND COALESCE(s.is_active, TRUE) = TRUE
      AND s.online_status = 'online'
      AND s.uid IS NOT NULL
      AND (p_station_id IS NULL OR s.station_id = p_station_id)
    ORDER BY COALESCE(s.updated_at, s.created_at) DESC, s.created_at DESC
    LIMIT 1;

    RETURN v_candidate;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_homecare_dispatch_candidate(TEXT, UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_homecare_dispatch_candidate(TEXT, UUID) TO authenticated;-- RPC: rpc_admin_delete_agent
-- 管理端删除代理商(支持级联软删除代理申请记录)

CREATE OR REPLACE FUNCTION public.rpc_admin_delete_agent(
  p_uid uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_ok boolean;
  v_user_id UUID;
BEGIN
  -- 1. 权限检查
  IF NOT public.check_admin_permission('distribution:agent:delete') THEN
    RAISE EXCEPTION 'Permission denied: distribution:agent:delete';
  END IF;

  -- 2. 获取当前操作用户 ID
  SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

  -- 3. 级联软删除:该代理的所有申请记录
  UPDATE public.ak_distribution_agent_applications
  SET deleted_at = now(),
      deleted_by = v_user_id
  WHERE user_id = p_uid AND deleted_at IS NULL;

  -- 4. 软删除代理商记录
  UPDATE public.ak_distribution_agents
  SET deleted_at = now(),
      deleted_by = v_user_id
  WHERE uid = p_uid AND deleted_at IS NULL;
  
  GET DIAGNOSTICS v_ok = ROW_COUNT;
  RETURN v_ok;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_delete_agent(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_agent(uuid) TO authenticated;
-- RPC: rpc_admin_delete_division
-- 管理端删除事业部(支持级联软删除关联代理)

CREATE OR REPLACE FUNCTION public.rpc_admin_delete_division(
  p_uid uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_ok boolean;
  v_user_id UUID;
BEGIN
  -- 1. 权限检查
  IF NOT public.check_admin_permission('distribution:division:delete') THEN
    RAISE EXCEPTION 'Permission denied: distribution:division:delete';
  END IF;

  -- 2. 获取当前操作用户 ID
  SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

  -- 3. 级联软删除:该事业部下的所有代理商
  UPDATE public.ak_distribution_agents
  SET deleted_at = now(),
      deleted_by = v_user_id
  WHERE division_uid = p_uid AND deleted_at IS NULL;

  -- 4. 级联软删除:该事业部的所有申请记录
  UPDATE public.ak_distribution_division_applications
  SET deleted_at = now(),
      deleted_by = v_user_id
  WHERE user_id = p_uid AND deleted_at IS NULL;

  -- 5. 软删除事业部本身
  UPDATE public.ak_distribution_divisions
  SET deleted_at = now(),
      deleted_by = v_user_id
  WHERE uid = p_uid AND deleted_at IS NULL;
  
  GET DIAGNOSTICS v_ok = ROW_COUNT;
  RETURN v_ok;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_delete_division(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_division(uuid) TO authenticated;
-- RPC: rpc_admin_get_agent_apply_list
-- 管理端获取代理商申请列表
-- 支持按状态过滤all, pending, approved, rejected

CREATE OR REPLACE FUNCTION public.rpc_admin_get_agent_apply_list(
  p_status text DEFAULT 'all',
  p_search text DEFAULT NULL,
  p_page integer DEFAULT 1,
  p_page_size integer DEFAULT 20
)
RETURNS TABLE (
  id uuid,
  uid uuid,
  name text,
  phone text,
  dept_uid uuid,
  dept_name text,
  proof_images jsonb,
  status text,
  refusal_reason text,
  time timestamptz,
  invite_code text
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_page integer := GREATEST(1, COALESCE(p_page, 1));
  v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
  v_offset integer := (v_page - 1) * v_page_size;
BEGIN
  -- 权限检查
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  RETURN QUERY
  SELECT 
    a.id,
    a.uid,
    a.agent_name AS name,
    a.agent_phone AS phone,
    a.division_uid AS dept_uid,
    d.name AS dept_name,
    a.proof_images,
    a.status,
    a.refusal_reason,
    a.created_at AS time,
    d.invite_code
  FROM public.ak_distribution_agent_applications a
  JOIN public.ak_distribution_divisions d ON d.uid = a.division_uid
  WHERE (p_status = 'all' OR a.status = p_status)
    AND (
      p_search IS NULL OR p_search = ''
      OR a.agent_name ILIKE ('%' || p_search || '%')
      OR a.uid::text ILIKE ('%' || p_search || '%')
    )
  ORDER BY a.created_at DESC
  LIMIT v_page_size OFFSET v_offset;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_get_agent_apply_list(text, text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_agent_apply_list(text, text, integer, integer) TO authenticated;
-- RPC: rpc_admin_get_agent_list
-- 管理端获取代理商列表
-- 支持搜索代理商名称或负责人UID并关联显示所属事业部信息

CREATE OR REPLACE FUNCTION public.rpc_admin_get_agent_list(
  p_search text DEFAULT NULL,
  p_page integer DEFAULT 1,
  p_page_size integer DEFAULT 20
)
RETURNS TABLE (
  uid uuid,
  name text,
  division_uid uuid,
  division_name text,
  commission_ratio numeric,
  is_enabled boolean,
  end_time timestamptz,
  created_at timestamptz,
  "staffCount" bigint
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_page integer := GREATEST(1, COALESCE(p_page, 1));
  v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
  v_offset integer := (v_page - 1) * v_page_size;
BEGIN
  -- 权限检查
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  RETURN QUERY
  SELECT 
    a.uid,
    a.name,
    a.division_uid,
    d.name AS division_name,
    a.commission_ratio,
    a.is_enabled,
    a.end_time,
    a.created_at,
    (SELECT COUNT(*) FROM public.ak_promoter_relations r WHERE r.inviter_uid = a.uid)::bigint AS "staffCount"
  FROM public.ak_distribution_agents a
  JOIN public.ak_distribution_divisions d ON d.uid = a.division_uid
  WHERE (
    p_search IS NULL OR p_search = ''
    OR a.name ILIKE ('%' || p_search || '%')
    OR a.uid::text ILIKE ('%' || p_search || '%')
  )
  ORDER BY a.created_at DESC
  LIMIT v_page_size OFFSET v_offset;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_get_agent_list(text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_agent_list(text, integer, integer) TO authenticated;
-- RPC: rpc_admin_get_division_list
-- 管理端获取事业部列表
-- 支持搜索事业部名称或负责人UID

CREATE OR REPLACE FUNCTION public.rpc_admin_get_division_list(
  p_search text DEFAULT NULL,
  p_page integer DEFAULT 1,
  p_page_size integer DEFAULT 20
)
RETURNS TABLE (
  uid uuid,
  name text,
  invite_code text,
  commission_ratio numeric,
  is_enabled boolean,
  end_time timestamptz,
  created_at timestamptz,
  "agentCount" bigint
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_page integer := GREATEST(1, COALESCE(p_page, 1));
  v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
  v_offset integer := (v_page - 1) * v_page_size;
BEGIN
  -- 仅管理员或分析员可调用
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  RETURN QUERY
  SELECT 
    d.uid,
    d.name,
    d.invite_code,
    d.commission_ratio,
    d.is_enabled,
    d.end_time,
    d.created_at,
    (SELECT COUNT(*) FROM public.ak_distribution_agents a WHERE a.division_uid = d.uid)::bigint AS "agentCount"
  FROM public.ak_distribution_divisions d
  WHERE (
    p_search IS NULL OR p_search = ''
    OR d.name ILIKE ('%' || p_search || '%')
    OR d.uid::text ILIKE ('%' || p_search || '%')
  )
  ORDER BY d.created_at DESC
  LIMIT v_page_size OFFSET v_offset;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_get_division_list(text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_division_list(text, integer, integer) TO authenticated;
-- RPC: rpc_admin_get_promoter_list
-- 管理端推广员列表聚合统计
-- 口径:集合=B上级+下级都算)=> 关系表中出现过的 uid/inviter_uid 都算推广员候选
-- 统计:
--  - 推广用户数量:以该用户作为 inviter_uid 的下级人数
--  - 推广订单数量/金额:其下级用户在 ml_orders 中已完成(order_status=4)的订单数与 paid_amount 汇总
--  - 佣金:从 ak_commission_logs 聚合

CREATE OR REPLACE FUNCTION public.rpc_admin_get_promoter_list(
  p_search text DEFAULT NULL,
  p_page integer DEFAULT 1,
  p_page_size integer DEFAULT 20,
  p_start_time timestamptz DEFAULT NULL,
  p_end_time timestamptz DEFAULT NULL
)
RETURNS TABLE (
  id uuid,
  nickname text,
  name text,
  phone text,
  avatar_url text,
  level text,
  "userCount" bigint,
  "orderCount" bigint,
  "orderAmount" numeric,
  "commissionTotal" numeric,
  "withdrawnAmount" numeric,
  "withdrawCount" bigint,
  "unwithdrawnAmount" numeric
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_page integer := GREATEST(1, COALESCE(p_page, 1));
  v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
  v_offset integer := (v_page - 1) * v_page_size;
BEGIN
  -- 仅管理员可调用
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role = 'admin'
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  RETURN QUERY
  WITH promoters AS (
    SELECT DISTINCT x.uid
    FROM (
      SELECT r.uid FROM public.ak_promoter_relations r
      UNION
      SELECT r.inviter_uid FROM public.ak_promoter_relations r
    ) x
  ),
  base AS (
    SELECT
      u.id,
      u.username AS nickname,
      u.real_name AS name,
      u.phone,
      u.avatar_url,
      u.role AS level
    FROM promoters p
    JOIN public.ak_users u ON u.id = p.uid
    WHERE (
      p_search IS NULL OR p_search = ''
      OR u.username ILIKE ('%' || p_search || '%')
      OR COALESCE(u.real_name, '') ILIKE ('%' || p_search || '%')
      OR COALESCE(u.phone, '') ILIKE ('%' || p_search || '%')
      OR u.id::text ILIKE ('%' || p_search || '%')
    )
  ),
  downline AS (
    SELECT inviter_uid, uid
    FROM public.ak_promoter_relations
  ),
  user_stats AS (
    SELECT
      d.inviter_uid AS id,
      COUNT(*)::bigint AS "userCount"
    FROM downline d
    GROUP BY d.inviter_uid
  ),
  order_stats AS (
    SELECT
      d.inviter_uid AS id,
      COUNT(o.id)::bigint AS "orderCount",
      COALESCE(SUM(o.paid_amount), 0)::numeric AS "orderAmount"
    FROM downline d
    JOIN public.ml_orders o ON o.user_id = d.uid
    WHERE o.order_status = 4
      AND (p_start_time IS NULL OR o.completed_at >= p_start_time)
      AND (p_end_time IS NULL OR o.completed_at <= p_end_time)
    GROUP BY d.inviter_uid
  ),
  commission_stats AS (
    SELECT
      c.uid AS id,
      COALESCE(SUM(c.amount), 0)::numeric AS "commissionTotal",
      COALESCE(SUM(CASE WHEN c.status = 'withdrawn' THEN c.amount ELSE 0 END), 0)::numeric AS "withdrawnAmount",
      0::bigint AS "withdrawCount",
      COALESCE(SUM(CASE WHEN c.status IN ('frozen','available') THEN c.amount ELSE 0 END), 0)::numeric AS "unwithdrawnAmount"
    FROM public.ak_commission_logs c
    GROUP BY c.uid
  )
  SELECT
    b.id,
    b.nickname,
    b.name,
    b.phone,
    b.avatar_url,
    b.level,
    COALESCE(us."userCount", 0) AS "userCount",
    COALESCE(os."orderCount", 0) AS "orderCount",
    COALESCE(os."orderAmount", 0) AS "orderAmount",
    COALESCE(cs."commissionTotal", 0) AS "commissionTotal",
    COALESCE(cs."withdrawnAmount", 0) AS "withdrawnAmount",
    COALESCE(cs."withdrawCount", 0) AS "withdrawCount",
    COALESCE(cs."unwithdrawnAmount", 0) AS "unwithdrawnAmount"
  FROM base b
  LEFT JOIN user_stats us ON us.id = b.id
  LEFT JOIN order_stats os ON os.id = b.id
  LEFT JOIN commission_stats cs ON cs.id = b.id
  ORDER BY b.id
  LIMIT v_page_size OFFSET v_offset;
END;
$$;

-- 授权:仅允许 authenticated 调用,函数内部再做 admin 校验
REVOKE ALL ON FUNCTION public.rpc_admin_get_promoter_list(text, integer, integer, timestamptz, timestamptz) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_promoter_list(text, integer, integer, timestamptz, timestamptz) TO authenticated;
-- RPC: rpc_admin_process_agent_apply
-- 管理端审核代理商申请
-- 若通过(approved),则同步在 ak_distribution_agents 中创建或更新记录

CREATE OR REPLACE FUNCTION public.rpc_admin_process_agent_apply(
  p_id uuid,
  p_status text, -- approved / rejected
  p_refusal_reason text DEFAULT NULL
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
  v_uid uuid;
  v_division_uid uuid;
  v_agent_name text;
BEGIN
  -- 仅管理员可审核
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role = 'admin'
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  -- 1. 获取并锁定申请记录
  SELECT uid, division_uid, agent_name 
  INTO v_uid, v_division_uid, v_agent_name
  FROM public.ak_distribution_agent_applications
  WHERE id = p_id;

  IF v_uid IS NULL THEN
    RAISE EXCEPTION 'application record not found';
  END IF;

  -- 2. 更新申请状态
  UPDATE public.ak_distribution_agent_applications
  SET 
    status = p_status,
    refusal_reason = CASE WHEN p_status = 'rejected' THEN p_refusal_reason ELSE NULL END,
    approved_at = now(),
    approved_by = auth.uid(),
    updated_at = now()
  WHERE id = p_id;

  -- 3. 如果通过,则同步到代理商正式表
  IF p_status = 'approved' THEN
    INSERT INTO public.ak_distribution_agents (
      uid, division_uid, name, commission_ratio, is_enabled, updated_at, updated_by
    )
    VALUES (
      v_uid, v_division_uid, v_agent_name, 0, true, now(), auth.uid()
    )
    ON CONFLICT (uid) DO UPDATE
    SET
      division_uid = EXCLUDED.division_uid,
      name = EXCLUDED.name,
      updated_at = now(),
      updated_by = auth.uid();
  END IF;

  RETURN true;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_process_agent_apply(uuid, text, text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_process_agent_apply(uuid, text, text) TO authenticated;
-- RPC: rpc_admin_save_agent
-- 管理端新增或更新代理商

CREATE OR REPLACE FUNCTION public.rpc_admin_save_agent(
  p_uid uuid,
  p_division_uid uuid,
  p_name text,
  p_commission_ratio numeric,
  p_is_enabled boolean DEFAULT true,
  p_end_time timestamptz DEFAULT NULL
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
  -- 仅管理员可操作
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role = 'admin'
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  -- 确保事业部存在
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_distribution_divisions WHERE uid = p_division_uid
  ) THEN
    RAISE EXCEPTION 'parent division not found';
  END IF;

  INSERT INTO public.ak_distribution_agents (
    uid, division_uid, name, commission_ratio, is_enabled, end_time, updated_at, updated_by
  )
  VALUES (
    p_uid, p_division_uid, p_name, p_commission_ratio, p_is_enabled, p_end_time, now(), auth.uid()
  )
  ON CONFLICT (uid) DO UPDATE
  SET
    division_uid = EXCLUDED.division_uid,
    name = EXCLUDED.name,
    commission_ratio = EXCLUDED.commission_ratio,
    is_enabled = EXCLUDED.is_enabled,
    end_time = EXCLUDED.end_time,
    updated_at = now(),
    updated_by = auth.uid();

  RETURN p_uid;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_save_agent(uuid, uuid, text, numeric, boolean, timestamptz) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_agent(uuid, uuid, text, numeric, boolean, timestamptz) TO authenticated;
-- RPC: rpc_admin_save_division
-- 管理端新增或更新事业部

CREATE OR REPLACE FUNCTION public.rpc_admin_save_division(
  p_uid uuid,
  p_name text,
  p_invite_code text,
  p_commission_ratio numeric,
  p_is_enabled boolean DEFAULT true,
  p_end_time timestamptz DEFAULT NULL
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
  -- 仅管理员可操作
  IF NOT EXISTS (
    SELECT 1 FROM public.ak_users u
    WHERE u.id = auth.uid() AND u.role = 'admin'
  ) THEN
    RAISE EXCEPTION 'permission denied';
  END IF;

  INSERT INTO public.ak_distribution_divisions (
    uid, name, invite_code, commission_ratio, is_enabled, end_time, updated_at, updated_by
  )
  VALUES (
    p_uid, p_name, p_invite_code, p_commission_ratio, p_is_enabled, p_end_time, now(), auth.uid()
  )
  ON CONFLICT (uid) DO UPDATE
  SET
    name = EXCLUDED.name,
    invite_code = EXCLUDED.invite_code,
    commission_ratio = EXCLUDED.commission_ratio,
    is_enabled = EXCLUDED.is_enabled,
    end_time = EXCLUDED.end_time,
    updated_at = now(),
    updated_by = auth.uid();

  RETURN p_uid;
END;
$$;

REVOKE ALL ON FUNCTION public.rpc_admin_save_division(uuid, text, text, numeric, boolean, timestamptz) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_division(uuid, text, text, numeric, boolean, timestamptz) TO authenticated;
-- =====================================================================================
-- Admin 财务统计 - 余额收支分布统计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按业务子类型统计指定时间范围内的余额收入与支出分布
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_balance_distribution(
    p_start_time TIMESTAMP WITH TIME ZONE,
    p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_total_income DECIMAL(12,2);
    v_total_expense DECIMAL(12,2);
    v_income_items JSONB;
    v_expense_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 计算总收入与总支出
    SELECT 
        COALESCE(SUM(number) FILTER (WHERE pm = 1), 0),
        COALESCE(SUM(number) FILTER (WHERE pm = 0), 0)
    INTO v_total_income, v_total_expense
    FROM public.ml_user_bill
    WHERE category = 'balance' 
      AND created_at >= p_start_time 
      AND created_at <= p_end_time
      AND status = 1;

    -- 3. 统计收入分布 (来源分析)
    SELECT jsonb_agg(t) INTO v_income_items
    FROM (
        SELECT 
            type AS name,
            SUM(number) AS value,
            CASE WHEN v_total_income > 0 THEN ROUND(SUM(number) / v_total_income * 100, 2) ELSE 0 END AS percent
        FROM public.ml_user_bill
        WHERE category = 'balance' AND pm = 1 AND status = 1
          AND created_at >= p_start_time AND created_at <= p_end_time
        GROUP BY type
        ORDER BY value DESC
    ) t;

    -- 4. 统计支出分布 (消耗分析)
    SELECT jsonb_agg(t) INTO v_expense_items
    FROM (
        SELECT 
            type AS name,
            SUM(number) AS value,
            CASE WHEN v_total_expense > 0 THEN ROUND(SUM(number) / v_total_expense * 100, 2) ELSE 0 END AS percent
        FROM public.ml_user_bill
        WHERE category = 'balance' AND pm = 0 AND status = 1
          AND created_at >= p_start_time AND created_at <= p_end_time
        GROUP BY type
        ORDER BY value DESC
    ) t;

    RETURN jsonb_build_object(
        'income', COALESCE(v_income_items, '[]'::jsonb),
        'expense', COALESCE(v_expense_items, '[]'::jsonb)
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_balance_distribution IS '统计财务余额收支来源与消耗分布';
-- =====================================================================================
-- Admin 财务统计 - 余额核心指标 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:获取全站当前余额存量、累计增加总额及累计消耗总额
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_balance_stats()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_current_balance DECIMAL(12,2);
    v_total_accumulation DECIMAL(12,2);
    v_total_consumption DECIMAL(12,2);
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 统计当前全站用户余额总存量
    SELECT COALESCE(SUM(now_money), 0) INTO v_current_balance FROM public.ak_users;

    -- 3. 统计累计增加 (pm=1) 和 累计消耗 (pm=0)
    -- 基于 ml_user_bill 表中 category='balance' 的记录
    SELECT 
        COALESCE(SUM(number) FILTER (WHERE pm = 1), 0),
        COALESCE(SUM(number) FILTER (WHERE pm = 0), 0)
    INTO v_total_accumulation, v_total_consumption
    FROM public.ml_user_bill
    WHERE category = 'balance' AND status = 1;

    RETURN jsonb_build_object(
        'current_balance', v_current_balance,
        'total_accumulation', v_total_accumulation,
        'total_consumption', v_total_consumption
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_balance_stats IS '获取全站余额存量及累计收支汇总';
-- =====================================================================================
-- Admin 财务统计 - 余额收支趋势 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按日聚合指定时间范围内的余额积累 (pm=1) 与 余额消耗 (pm=0) 趋势
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_balance_trend(
    p_start_time TIMESTAMP WITH TIME ZONE,
    p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 按日聚合统计
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT
            to_char(date_trunc('day', gs.day), 'YYYY-MM-DD') AS date_group,
            COALESCE(SUM(number) FILTER (WHERE pm = 1 AND category = 'balance'), 0) AS accumulation,
            COALESCE(SUM(number) FILTER (WHERE pm = 0 AND category = 'balance'), 0) AS consumption
        FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
        LEFT JOIN public.ml_user_bill b ON date_trunc('day', b.created_at) = gs.day AND b.status = 1
        GROUP BY gs.day
        ORDER BY gs.day ASC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_balance_trend IS '按日聚合财务余额收支趋势';
-- =====================================================================================
-- Admin 财务功能 - 提现申请列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_extract, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_extract_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_status SMALLINT DEFAULT NULL,
    p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
    p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_extract e
    LEFT JOIN public.ak_users u ON u.id = e.uid
    WHERE (p_status IS NULL OR e.status = p_status)
      AND (p_start_time IS NULL OR e.created_at >= p_start_time)
      AND (p_end_time IS NULL OR e.created_at <= p_end_time)
      AND (p_search IS NULL OR (
          COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
          COALESCE(u.email, '') ILIKE '%' || p_search || '%' OR
          COALESCE(e.real_name, '') ILIKE '%' || p_search || '%' OR
          COALESCE(e.bank_code, '') ILIKE '%' || p_search || '%' OR
          COALESCE(e.alipay_code, '') ILIKE '%' || p_search || '%' OR
          COALESCE(e.wechat_code, '') ILIKE '%' || p_search || '%'
      ));

    -- 3. 获取明细数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            e.id,
            e.uid,
            e.real_name,
            e.extract_type,
            e.bank_code,
            e.bank_address,
            e.alipay_code,
            e.wechat_code,
            e.extract_price,
            e.service_fee,
            e.balance,
            e.status,
            e.refusal_reason,
            e.admin_id,
            e.payment_time,
            e.created_at,
            e.updated_at,
            u.username as user_name,
            u.email as user_email
        FROM public.ml_extract e
        LEFT JOIN public.ak_users u ON u.id = e.uid
        WHERE (p_status IS NULL OR e.status = p_status)
          AND (p_start_time IS NULL OR e.created_at >= p_start_time)
          AND (p_end_time IS NULL OR e.created_at <= p_end_time)
          AND (p_search IS NULL OR (
              COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
              COALESCE(u.email, '') ILIKE '%' || p_search || '%' OR
              COALESCE(e.real_name, '') ILIKE '%' || p_search || '%' OR
              COALESCE(e.bank_code, '') ILIKE '%' || p_search || '%' OR
              COALESCE(e.alipay_code, '') ILIKE '%' || p_search || '%' OR
              COALESCE(e.wechat_code, '') ILIKE '%' || p_search || '%'
          ))
        ORDER BY e.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    -- 4. 返回结果
    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_extract_list IS '管理员提现申请列表分页查询';
-- =====================================================================================
-- Admin 财务功能 - 提现审核 RPC (口径 2)
-- 位置docs/sql/30_rpc/finance/
-- 版本v1
-- 描述:提现审核通过时才扣除佣金并生成流水。
-- 安全策略SECURITY DEFINER, 入口鉴权, 固定 search_path
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_extract_review(
    p_extract_id UUID,
    p_status SMALLINT, -- 1: 通过, -1: 驳回
    p_refusal_reason TEXT DEFAULT NULL
)
RETURNS VOID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_extract RECORD;
    v_user RECORD;
BEGIN
    -- 1. 鉴权:仅 admin 角色可执行
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 锁定并获取提现记录
    SELECT * INTO v_extract FROM public.ml_extract WHERE id = p_extract_id FOR UPDATE;
    IF NOT FOUND THEN RAISE EXCEPTION 'Extract record not found'; END IF;
    IF v_extract.status != 0 THEN RAISE EXCEPTION 'Record already processed'; END IF;

    -- 3. 业务处理
    IF p_status = 1 THEN
        -- 审核通过:锁定并校验用户资金
        SELECT * INTO v_user FROM public.ak_users WHERE id = v_extract.uid FOR UPDATE;
        IF v_user.brokerage_price < v_extract.extract_price THEN
            RAISE EXCEPTION 'Insufficient brokerage balance';
        END IF;

        -- 扣除佣金
        UPDATE public.ak_users 
        SET brokerage_price = brokerage_price - v_extract.extract_price 
        WHERE id = v_extract.uid;

        -- 写入资金流水
        INSERT INTO public.ml_user_bill (uid, link_id, pm, title, category, type, number, balance)
        VALUES (
            v_extract.uid, 
            p_extract_id::TEXT, 
            0, -- 支出
            '佣金提现', 
            'brokerage', 
            'extract', 
            v_extract.extract_price,
            v_user.brokerage_price - v_extract.extract_price
        );

        -- 更新提现记录
        UPDATE public.ml_extract 
        SET status = 1, admin_id = auth.uid(), payment_time = now() 
        WHERE id = p_extract_id;

    ELSIF p_status = -1 THEN
        -- 审核驳回:仅更新状态
        UPDATE public.ml_extract 
        SET status = -1, refusal_reason = p_refusal_reason, admin_id = auth.uid() 
        WHERE id = p_extract_id;
    ELSE
        RAISE EXCEPTION 'Invalid status';
    END IF;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_extract_review IS '管理员审核提现申请(口径 2通过时扣款';
-- =====================================================================================
-- Admin 财务功能 - 账单汇总统计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按日/周/月维度聚合财务收支数据,支撑账单列表展示
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_finance_bill_summary(
    p_start_time TIMESTAMP WITH TIME ZONE,
    p_end_time TIMESTAMP WITH TIME ZONE,
    p_interval TEXT DEFAULT 'day' -- day, week, month
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 聚合统计
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT
            to_char(date_trunc(p_interval, created_at), 
                CASE 
                    WHEN p_interval = 'day' THEN 'YYYY-MM-DD'
                    WHEN p_interval = 'week' THEN 'IYYY-IW'
                    ELSE 'YYYY-MM'
                END
            ) AS date_group,
            SUM(number) FILTER (WHERE pm = 1) AS income,
            SUM(number) FILTER (WHERE pm = 0) AS expense,
            SUM(CASE WHEN pm = 1 THEN number ELSE -number END) AS net_entry
        FROM public.ml_user_bill
        WHERE created_at >= p_start_time
          AND created_at <= p_end_time
          AND status = 1
        GROUP BY date_trunc(p_interval, created_at)
        ORDER BY date_trunc(p_interval, created_at) DESC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_finance_bill_summary IS '按周期聚合财务收支账单';
-- =====================================================================================
-- Admin 财务功能 - 财务概况统计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:获取指定时间段内的财务核心 KPI营业额、充值汇总、提现汇总、资金存量
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_finance_overview(
    p_start_time TIMESTAMP WITH TIME ZONE,
    p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_recharge_amount DECIMAL(12,2);
    v_recharge_count BIGINT;
    v_extract_amount DECIMAL(12,2);
    v_extract_count BIGINT;
    v_total_user_balance DECIMAL(12,2);
    v_total_user_brokerage DECIMAL(12,2);
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 统计充值 (仅统计已支付)
    SELECT 
        COALESCE(SUM(price + give_price), 0),
        COUNT(*)
    INTO v_recharge_amount, v_recharge_count
    FROM public.ml_user_recharge
    WHERE paid = 1
      AND created_at >= p_start_time
      AND created_at <= p_end_time;

    -- 3. 统计提现 (仅统计已通过)
    SELECT 
        COALESCE(SUM(extract_price), 0),
        COUNT(*)
    INTO v_extract_amount, v_extract_count
    FROM public.ml_extract
    WHERE status = 1
      AND created_at >= p_start_time
      AND created_at <= p_end_time;

    -- 4. 统计全站资金存量 (实时快照)
    SELECT 
        COALESCE(SUM(now_money), 0),
        COALESCE(SUM(brokerage_price), 0)
    INTO v_total_user_balance, v_total_user_brokerage
    FROM public.ak_users;

    RETURN jsonb_build_object(
        'recharge_amount', v_recharge_amount,
        'recharge_count', v_recharge_count,
        'extract_amount', v_extract_amount,
        'extract_count', v_extract_count,
        'total_user_balance', v_total_user_balance,
        'total_user_brokerage', v_total_user_brokerage
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_finance_overview IS '财务核心 KPI 概况统计';
-- =====================================================================================
-- RPC: rpc_admin_invoice_list
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取发票申请列表,支持搜索、状态筛选及时间过滤
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_invoice_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_status SMALLINT DEFAULT NULL,
    p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
    p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_invoices i
    LEFT JOIN public.ak_users u ON u.id = i.uid
    WHERE (p_status IS NULL OR i.status = p_status)
      AND (p_start_time IS NULL OR i.created_at >= p_start_time)
      AND (p_end_time IS NULL OR i.created_at <= p_end_time)
      AND (p_search IS NULL OR (
          i.order_no ILIKE '%' || p_search || '%' OR
          i.header_name ILIKE '%' || p_search || '%' OR
          u.username ILIKE '%' || p_search || '%'
      ));

    -- 3. 获取明细数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            i.id,
            i.uid,
            i.order_no,
            i.order_amount,
            i.invoice_type,
            i.header_type,
            i.header_name,
            i.tax_id,
            i.email,
            i.remark,
            i.status,
            i.refusal_reason,
            i.invoice_url,
            i.created_at,
            i.updated_at,
            u.username as user_name,
            u.email as user_email
        FROM public.ml_invoices i
        LEFT JOIN public.ak_users u ON u.id = i.uid
        WHERE (p_status IS NULL OR i.status = p_status)
          AND (p_start_time IS NULL OR i.created_at >= p_start_time)
          AND (p_end_time IS NULL OR i.created_at <= p_end_time)
          AND (p_search IS NULL OR (
              i.order_no ILIKE '%' || p_search || '%' OR
              i.header_name ILIKE '%' || p_search || '%' OR
              u.username ILIKE '%' || p_search || '%'
          ))
        ORDER BY i.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_invoice_list IS '管理员分页查询发票申请列表';
-- =====================================================================================
-- RPC: rpc_admin_invoice_process
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端处理发票申请(开票或驳回)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_invoice_process(
    p_id UUID,
    p_status SMALLINT, -- 1: 已开票, -1: 已拒绝
    p_invoice_url TEXT DEFAULT NULL,
    p_refusal_reason TEXT DEFAULT NULL
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 更新状态
    UPDATE public.ml_invoices
    SET 
        status = p_status,
        invoice_url = CASE WHEN p_status = 1 THEN p_invoice_url ELSE invoice_url END,
        refusal_reason = CASE WHEN p_status = -1 THEN p_refusal_reason ELSE refusal_reason END,
        updated_at = now()
    WHERE id = p_id;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_invoice_process IS '管理员处理发票开票申请';
-- =====================================================================================
-- Admin 财务功能 - 充值补单/审计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 版本v1
-- 描述:由管理员发起的人工充值补单或离线支付审计确认。
-- 安全策略SECURITY DEFINER, 入口鉴权, 固定 search_path
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_recharge_audit(
    p_recharge_id UUID,
    p_mark TEXT DEFAULT '管理员人工审计/补单'
)
RETURNS VOID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_recharge RECORD;
    v_user RECORD;
BEGIN
    -- 1. 鉴权:仅 admin 角色可执行
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 锁定并获取充值记录
    SELECT * INTO v_recharge FROM public.ml_user_recharge WHERE id = p_recharge_id FOR UPDATE;
    IF NOT FOUND THEN RAISE EXCEPTION 'Recharge record not found'; END IF;
    IF v_recharge.paid = 1 THEN RAISE EXCEPTION 'Recharge already paid'; END IF;

    -- 3. 锁定并更新用户余额
    SELECT * INTO v_user FROM public.ak_users WHERE id = v_recharge.uid FOR UPDATE;
    
    UPDATE public.ak_users 
    SET now_money = now_money + v_recharge.price + v_recharge.give_price 
    WHERE id = v_recharge.uid;

    -- 4. 写入资金流水
    INSERT INTO public.ml_user_bill (uid, link_id, pm, title, category, type, number, balance, mark)
    VALUES (
        v_recharge.uid, 
        v_recharge.order_no, 
        1, -- 收入
        '用户充值', 
        'now_money', 
        'recharge', 
        v_recharge.price + v_recharge.give_price,
        v_user.now_money + v_recharge.price + v_recharge.give_price,
        p_mark
    );

    -- 5. 更新充值记录状态
    UPDATE public.ml_user_recharge 
    SET paid = 1, pay_time = now() 
    WHERE id = p_recharge_id;

END;
$$;

COMMENT ON FUNCTION public.rpc_admin_recharge_audit IS '管理员人工审计/补单(更新用户余额并生成流水)';
-- =====================================================================================
-- Admin 财务功能 - 充值记录列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_user_recharge, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_recharge_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_paid SMALLINT DEFAULT NULL,
    p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
    p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_user_recharge r
    LEFT JOIN public.ak_users u ON u.id = r.uid
    WHERE (p_paid IS NULL OR r.paid = p_paid)
      AND (p_start_time IS NULL OR r.created_at >= p_start_time)
      AND (p_end_time IS NULL OR r.created_at <= p_end_time)
      AND (p_search IS NULL OR (
          COALESCE(r.order_no, '') ILIKE '%' || p_search || '%' OR
          COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
          COALESCE(u.email, '') ILIKE '%' || p_search || '%'
      ));

    -- 3. 获取明细数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            r.id,
            r.uid,
            r.order_no,
            r.recharge_type,
            r.price,
            r.give_price,
            r.paid,
            r.pay_time,
            r.channel_trade_no,
            r.status,
            r.created_at,
            r.updated_at,
            u.username as user_name,
            u.email as user_email
        FROM public.ml_user_recharge r
        LEFT JOIN public.ak_users u ON u.id = r.uid
        WHERE (p_paid IS NULL OR r.paid = p_paid)
          AND (p_start_time IS NULL OR r.created_at >= p_start_time)
          AND (p_end_time IS NULL OR r.created_at <= p_end_time)
          AND (p_search IS NULL OR (
              COALESCE(r.order_no, '') ILIKE '%' || p_search || '%' OR
              COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
              COALESCE(u.email, '') ILIKE '%' || p_search || '%'
          ))
        ORDER BY r.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    -- 4. 返回结果
    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_recharge_list IS '管理员充值记录列表分页查询';
-- =====================================================================================
-- Admin 财务功能 - 资金流水列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_user_bill, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_bill_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_category VARCHAR DEFAULT NULL,
    p_type VARCHAR DEFAULT NULL,
    p_pm SMALLINT DEFAULT NULL,
    p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
    p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_user_bill b
    LEFT JOIN public.ak_users u ON u.id = b.uid
    WHERE (p_category IS NULL OR b.category = p_category)
      AND (p_type IS NULL OR b.type = p_type)
      AND (p_pm IS NULL OR b.pm = p_pm)
      AND (p_start_time IS NULL OR b.created_at >= p_start_time)
      AND (p_end_time IS NULL OR b.created_at <= p_end_time)
      AND (p_search IS NULL OR (
          COALESCE(b.title, '') ILIKE '%' || p_search || '%' OR
          COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
          COALESCE(u.email, '') ILIKE '%' || p_search || '%'
      ));

    -- 3. 获取明细数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            b.id,
            b.uid,
            b.link_id,
            b.pm,
            b.title,
            b.category,
            b.type,
            b.number,
            b.balance,
            b.mark,
            b.status,
            b.created_at,
            b.updated_at,
            u.username as user_name,
            u.email as user_email
        FROM public.ml_user_bill b
        LEFT JOIN public.ak_users u ON u.id = b.uid
        WHERE (p_category IS NULL OR b.category = p_category)
          AND (p_type IS NULL OR b.type = p_type)
          AND (p_pm IS NULL OR b.pm = p_pm)
          AND (p_start_time IS NULL OR b.created_at >= p_start_time)
          AND (p_end_time IS NULL OR b.created_at <= p_end_time)
          AND (p_search IS NULL OR (
              COALESCE(b.title, '') ILIKE '%' || p_search || '%' OR
              COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
              COALESCE(u.email, '') ILIKE '%' || p_search || '%'
          ))
        ORDER BY b.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    -- 4. 返回结果
    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_user_bill_list IS '管理员资金流水列表分页查询';
-- =====================================================================================
-- RPC: rpc_admin_kefu_account_delete
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除客服账号(使用通用权限校验)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('kefu:account:delete') THEN
        RAISE EXCEPTION 'Permission denied: kefu:account:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 执行软删除:标记 deleted_at
    UPDATE public.ml_kefu_accounts
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_account_delete IS '管理员删除客服账号';
-- =====================================================================================
-- RPC: rpc_admin_kefu_account_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取客服账号列表
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_search TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_kefu_accounts ka
    JOIN public.ak_users u ON u.id = ka.user_id
    WHERE (p_status IS NULL OR ka.status = p_status)
      AND (p_search IS NULL OR ka.nickname ILIKE '%' || p_search || '%' OR u.username ILIKE '%' || p_search || '%');

    -- 3. 获取数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            ka.id,
            ka.user_id,
            ka.nickname,
            ka.avatar,
            ka.status,
            ka.is_online,
            ka.created_at,
            ka.updated_at,
            u.username as user_account
        FROM public.ml_kefu_accounts ka
        JOIN public.ak_users u ON u.id = ka.user_id
        WHERE (p_status IS NULL OR ka.status = p_status)
          AND (p_search IS NULL OR ka.nickname ILIKE '%' || p_search || '%' OR u.username ILIKE '%' || p_search || '%')
        ORDER BY ka.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_kefu_account_save
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:新增或更新客服账号
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_save(
    p_id UUID DEFAULT NULL,
    p_user_id UUID DEFAULT NULL,
    p_nickname TEXT DEFAULT NULL,
    p_avatar TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 新增
    IF p_id IS NULL THEN
        IF p_user_id IS NULL OR p_nickname IS NULL THEN
            RAISE EXCEPTION 'Missing required fields';
        END IF;

        INSERT INTO public.ml_kefu_accounts (
            user_id, nickname, avatar, status
        ) VALUES (
            p_user_id, p_nickname, p_avatar, p_status
        ) RETURNING id INTO v_id;
    ELSE
        -- 3. 更新
        UPDATE public.ml_kefu_accounts
        SET 
            nickname = COALESCE(p_nickname, nickname),
            avatar = COALESCE(p_avatar, avatar),
            status = COALESCE(p_status, status),
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Account not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_kefu_account_set_status
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端切换客服账号启用/禁用状态
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_set_status(
    p_id UUID,
    p_status SMALLINT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 更新状态
    UPDATE public.ml_kefu_accounts
    SET status = p_status,
        updated_at = now()
    WHERE id = p_id;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_account_set_status IS '管理员设置客服账号状态';
-- =====================================================================================
-- RPC: rpc_admin_kefu_auto_reply_delete
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除客服自动回复配置(使用通用权限校验)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('kefu:auto_reply:delete') THEN
        RAISE EXCEPTION 'Permission denied: kefu:auto_reply:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 执行软删除:标记 deleted_at
    UPDATE public.ml_kefu_auto_replies
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_delete IS '管理员删除客服自动回复配置';
-- =====================================================================================
-- RPC: rpc_admin_kefu_auto_reply_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端获取客服自动回复配置列表
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_kefu_auto_replies
    WHERE (p_search IS NULL OR keyword ILIKE '%' || p_search || '%' OR content ILIKE '%' || p_search || '%');

    -- 3. 获取明细数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            id, keyword, content, reply_type, status,
            created_at, updated_at
        FROM public.ml_kefu_auto_replies
        WHERE (p_search IS NULL OR keyword ILIKE '%' || p_search || '%' OR content ILIKE '%' || p_search || '%')
        ORDER BY created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_list IS '管理员分页查询客服自动回复列表';
-- =====================================================================================
-- RPC: rpc_admin_kefu_auto_reply_save
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新自动回复配置
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_save(
    p_id UUID DEFAULT NULL,
    p_keyword TEXT DEFAULT NULL,
    p_content TEXT DEFAULT NULL,
    p_reply_type TEXT DEFAULT 'text',
    p_status SMALLINT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 新增
    IF p_id IS NULL THEN
        IF p_keyword IS NULL OR p_content IS NULL THEN
            RAISE EXCEPTION 'Missing required fields: keyword or content';
        END IF;

        INSERT INTO public.ml_kefu_auto_replies (
            keyword, content, reply_type, status
        ) VALUES (
            p_keyword, p_content, p_reply_type, p_status
        ) RETURNING id INTO v_id;
    ELSE
        -- 3. 更新
        UPDATE public.ml_kefu_auto_replies
        SET 
            keyword = COALESCE(p_keyword, keyword),
            content = COALESCE(p_content, content),
            reply_type = COALESCE(p_reply_type, reply_type),
            status = COALESCE(p_status, status),
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Auto reply record not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_save IS '管理员新增或更新自动回复配置';
-- =====================================================================================
-- RPC: rpc_admin_kefu_auto_reply_set_status
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端切换客服自动回复配置启用/禁用状态
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_set_status(
    p_id UUID,
    p_status SMALLINT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 更新状态
    UPDATE public.ml_kefu_auto_replies
    SET status = p_status,
        updated_at = now()
    WHERE id = p_id;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_set_status IS '管理员设置客服自动回复状态';
-- =====================================================================================
-- RPC: rpc_admin_kefu_feedback_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取用户留言反馈列表
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_feedback_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_search TEXT DEFAULT NULL,
    p_status SMALLINT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_kefu_feedbacks f
    LEFT JOIN public.ak_users u ON u.id = f.user_id
    WHERE (p_status IS NULL OR f.status = p_status)
      AND (p_search IS NULL OR f.nickname ILIKE '%' || p_search || '%' OR f.phone ILIKE '%' || p_search || '%' OR f.content ILIKE '%' || p_search || '%');

    -- 3. 获取数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            f.id,
            f.user_id,
            f.nickname,
            f.phone,
            f.content,
            f.status,
            f.reply_content,
            f.processed_at,
            f.created_at,
            f.updated_at,
            u.username as user_account
        FROM public.ml_kefu_feedbacks f
        LEFT JOIN public.ak_users u ON u.id = f.user_id
        WHERE (p_status IS NULL OR f.status = p_status)
          AND (p_search IS NULL OR f.nickname ILIKE '%' || p_search || '%' OR f.phone ILIKE '%' || p_search || '%' OR f.content ILIKE '%' || p_search || '%')
        ORDER BY f.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_feedback_list IS '管理员分页查询用户留言反馈列表';
-- =====================================================================================
-- RPC: rpc_admin_kefu_feedback_process
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端处理用户留言反馈(回复内容并更新状态)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_feedback_process(
    p_id UUID,
    p_reply_content TEXT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 更新状态
    UPDATE public.ml_kefu_feedbacks
    SET 
        status = 1, -- 已处理
        reply_content = p_reply_content,
        processed_at = now(),
        updated_at = now()
    WHERE id = p_id;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_feedback_process IS '管理员处理并回复用户留言反馈';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_category_delete
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除话术分类(支持级联软删除话术)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('kefu:word:category:delete') THEN
        RAISE EXCEPTION 'Permission denied: kefu:word:category:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 级联软删除:先标记该分类下的话术为删除
    UPDATE public.ml_kefu_words
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE category_id = p_id AND deleted_at IS NULL;

    -- 4. 执行软删除分类本身:标记 deleted_at
    UPDATE public.ml_kefu_word_categories
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_delete IS '管理员删除话术分类';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_category_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端获取话术分类列表
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_list()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取分类列表
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT id, name, sort, created_at, updated_at
        FROM public.ml_kefu_word_categories
        ORDER BY sort ASC, created_at DESC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_list IS '管理员获取话术分类列表';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_category_save
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新话术分类
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_save(
    p_id UUID DEFAULT NULL,
    p_name TEXT DEFAULT NULL,
    p_sort INTEGER DEFAULT 0
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 新增
    IF p_id IS NULL THEN
        IF p_name IS NULL THEN
            RAISE EXCEPTION 'Missing required fields: name';
        END IF;

        INSERT INTO public.ml_kefu_word_categories (
            name, sort
        ) VALUES (
            p_name, p_sort
        ) RETURNING id INTO v_id;
    ELSE
        -- 3. 更新
        UPDATE public.ml_kefu_word_categories
        SET 
            name = COALESCE(p_name, name),
            sort = COALESCE(p_sort, sort),
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Category not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_save IS '管理员新增或更新话术分类';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_delete
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除快捷话术(使用通用权限校验)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('kefu:word:delete') THEN
        RAISE EXCEPTION 'Permission denied: kefu:word:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 执行软删除:标记 deleted_at
    UPDATE public.ml_kefu_words
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_word_delete IS '管理员删除快捷话术';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端获取指定分类下的快捷话术列表
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_list(
    p_category_id UUID DEFAULT NULL,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取话术列表
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            w.id, 
            w.category_id, 
            w.title, 
            w.content, 
            w.sort, 
            w.created_at, 
            w.updated_at,
            c.name as category_name
        FROM public.ml_kefu_words w
        JOIN public.ml_kefu_word_categories c ON c.id = w.category_id
        WHERE (p_category_id IS NULL OR w.category_id = p_category_id)
          AND (p_search IS NULL OR w.title ILIKE '%' || p_search || '%' OR w.content ILIKE '%' || p_search || '%')
        ORDER BY w.sort ASC, w.created_at DESC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_word_list IS '管理员获取快捷话术列表';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_save
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新快捷话术
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_save(
    p_id UUID DEFAULT NULL,
    p_category_id UUID DEFAULT NULL,
    p_title TEXT DEFAULT NULL,
    p_content TEXT DEFAULT NULL,
    p_sort INTEGER DEFAULT 0
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 新增
    IF p_id IS NULL THEN
        IF p_category_id IS NULL OR p_title IS NULL OR p_content IS NULL THEN
            RAISE EXCEPTION 'Missing required fields';
        END IF;

        INSERT INTO public.ml_kefu_words (
            category_id, title, content, sort
        ) VALUES (
            p_category_id, p_title, p_content, p_sort
        ) RETURNING id INTO v_id;
    ELSE
        -- 3. 更新
        UPDATE public.ml_kefu_words
        SET 
            category_id = COALESCE(p_category_id, category_id),
            title = COALESCE(p_title, title),
            content = COALESCE(p_content, content),
            sort = COALESCE(p_sort, sort),
            updated_at = now()
        WHERE id = p_id
        RETURNING id INTO v_id;

        IF v_id IS NULL THEN
            RAISE EXCEPTION 'Word not found';
        END IF;
    END IF;

    RETURN v_id;
END;
$$;

COMMENT ON FUNCTION public.rpc_admin_kefu_word_save IS '管理员新增或更新快捷话术';
-- RPC: rpc_admin_get_integral_stats
-- 位置docs/sql/30_rpc/marketing/rpc_admin_get_integral_stats_v1.sql
-- 说明:聚合统计积分概况(总额、趋势、分布)

CREATE OR REPLACE FUNCTION public.rpc_admin_get_integral_stats(
    p_start_time TIMESTAMP WITH TIME ZONE,
    p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_total_stats RECORD;
    v_trend_data JSONB;
    v_source_dist JSONB;
    v_consume_dist JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 计算核心指标 (所有时间)
    SELECT 
        COALESCE(SUM(CASE WHEN pm = 1 THEN number ELSE -number END), 0) as current_total,
        COALESCE(SUM(CASE WHEN pm = 1 THEN number ELSE 0 END), 0) as cumulative_income,
        COALESCE(SUM(CASE WHEN pm = 0 THEN number ELSE 0 END), 0) as cumulative_expend
    INTO v_total_stats
    FROM public.ml_user_bill
    WHERE category = 'integral' AND status = 1;

    -- 3. 趋势数据 (按日聚合)
    SELECT jsonb_agg(t) INTO v_trend_data
    FROM (
        SELECT
            to_char(date_trunc('day', gs.day), 'MM-DD') AS date_group,
            COALESCE((SELECT SUM(number) FROM public.ml_user_bill b 
                      WHERE b.category = 'integral' AND b.pm = 1 AND b.status = 1 
                      AND date_trunc('day', b.created_at) = gs.day), 0) as income,
            COALESCE((SELECT SUM(number) FROM public.ml_user_bill b 
                      WHERE b.category = 'integral' AND b.pm = 0 AND b.status = 1 
                      AND date_trunc('day', b.created_at) = gs.day), 0) as expend
        FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
        ORDER BY gs.day ASC
    ) t;

    -- 4. 来源分布 (按 type 分组)
    SELECT jsonb_agg(t) INTO v_source_dist
    FROM (
        SELECT 
            type as label, 
            SUM(number) as value,
            ROUND((SUM(number) * 100 / NULLIF(v_total_stats.cumulative_income, 0)), 2) as percent
        FROM public.ml_user_bill
        WHERE category = 'integral' AND pm = 1 AND status = 1
        GROUP BY type
        ORDER BY value DESC
    ) t;

    -- 5. 消耗分布 (按 type 分组)
    SELECT jsonb_agg(t) INTO v_consume_dist
    FROM (
        SELECT 
            type as label, 
            SUM(number) as value,
            ROUND((SUM(number) * 100 / NULLIF(v_total_stats.cumulative_expend, 0)), 2) as percent
        FROM public.ml_user_bill
        WHERE category = 'integral' AND pm = 0 AND status = 1
        GROUP BY type
        ORDER BY value DESC
    ) t;

    RETURN jsonb_build_object(
        'totals', jsonb_build_object(
            'current', v_total_stats.current_total,
            'income', v_total_stats.cumulative_income,
            'expend', v_total_stats.cumulative_expend
        ),
        'trend', COALESCE(v_trend_data, '[]'::jsonb),
        'sources', COALESCE(v_source_dist, '[]'::jsonb),
        'consumes', COALESCE(v_consume_dist, '[]'::jsonb)
    );
END;
$$;

GRANT EXECUTE ON FUNCTION public.rpc_admin_get_integral_stats(timestamptz, timestamptz) TO authenticated;
-- =====================================================================================
-- Admin 订单功能 - 收银台订单列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_cashier_order_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_search_order_no TEXT DEFAULT NULL,
    p_search_username TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 总数:仅已支付订单
    SELECT COUNT(*) INTO v_total
    FROM public.ml_orders o
    LEFT JOIN public.ak_users u ON o.user_id = u.id
    WHERE o.paid_at IS NOT NULL
      AND (p_search_order_no IS NULL OR o.order_no ILIKE '%' || p_search_order_no || '%')
      AND (p_search_username IS NULL OR u.username ILIKE '%' || p_search_username || '%');

    -- 3. 明细
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            o.id,
            o.order_no,
            o.total_amount,
            o.discount_amount,
            o.paid_at,
            u.username as customer_name,
            u.phone as customer_phone
        FROM public.ml_orders o
        LEFT JOIN public.ak_users u ON o.user_id = u.id
        WHERE o.paid_at IS NOT NULL
          AND (p_search_order_no IS NULL OR o.order_no ILIKE '%' || p_search_order_no || '%')
          AND (p_search_username IS NULL OR u.username ILIKE '%' || p_search_username || '%')
        ORDER BY o.paid_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;-- =====================================================================================
-- Admin 订单管理 - 主订单列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_order_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_order_status INTEGER DEFAULT NULL,
    p_search TEXT DEFAULT NULL,
    p_start_time TIMESTAMPTZ DEFAULT NULL,
    p_end_time TIMESTAMPTZ DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查 (依赖 public.ak_users.role)
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_orders o
    LEFT JOIN public.ak_users u ON o.user_id = u.id
    WHERE (p_order_status IS NULL OR o.order_status = p_order_status)
      AND (p_start_time IS NULL OR o.created_at >= p_start_time)
      AND (p_end_time IS NULL OR o.created_at <= p_end_time)
      AND (
        p_search IS NULL 
        OR o.order_no ILIKE '%' || p_search || '%'
        OR u.username ILIKE '%' || p_search || '%'
        OR u.phone ILIKE '%' || p_search || '%'
      );

    -- 3. 获取明细列表
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            o.id,
            o.order_no,
            o.total_amount,
            o.paid_amount,
            o.discount_amount,
            o.order_status,
            o.payment_status,
            o.shipping_status,
            o.pay_type,
            o.channel_type,
            o.paid_at,
            o.created_at,
            u.username as buyer_name,
            u.phone as buyer_phone,
            (
                SELECT jsonb_build_object(
                    'product_name', oi.product_name,
                    'image_url', oi.image_url,
                    'quantity', oi.quantity
                )
                FROM public.ml_order_items oi
                WHERE oi.order_id = o.id
                ORDER BY oi.created_at ASC
                LIMIT 1
            ) as first_item_summary
        FROM public.ml_orders o
        LEFT JOIN public.ak_users u ON o.user_id = u.id
        WHERE (p_order_status IS NULL OR o.order_status = p_order_status)
          AND (p_start_time IS NULL OR o.created_at >= p_start_time)
          AND (p_end_time IS NULL OR o.created_at <= p_end_time)
          AND (
            p_search IS NULL 
            OR o.order_no ILIKE '%' || p_search || '%'
            OR u.username ILIKE '%' || p_search || '%'
            OR u.phone ILIKE '%' || p_search || '%'
          )
        ORDER BY o.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;
-- =====================================================================================
-- Admin 订单统计 - 订单来源分布 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:统计订单来源分布。
-- 注意:当前 ml_orders DDL 未包含来源/渠道字段,本函数提供最小可用兜底:统一返回 "unknown" 汇总。
--       若后续新增 channel/payment_method 等字段,可在此函数中替换为按渠道分组统计。
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_order_source_stats(
    p_start_time TIMESTAMPTZ,
    p_end_time TIMESTAMPTZ
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 按渠道类型聚合统计(排除已取消)
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT
            CASE o.channel_type
                WHEN 1 THEN '公众号'
                WHEN 2 THEN '小程序'
                WHEN 3 THEN 'H5'
                WHEN 4 THEN 'PC'
                WHEN 5 THEN 'APP'
                ELSE '其他'
            END AS source,
            COUNT(*) AS order_count,
            COALESCE(SUM(o.total_amount), 0) AS total_amount
        FROM public.ml_orders o
        WHERE o.created_at >= p_start_time
          AND o.created_at <= p_end_time
          AND o.order_status != 5
        GROUP BY o.channel_type
        ORDER BY order_count DESC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- Admin 订单统计 - 核心 KPI 汇总 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:获取指定时间段内的订单量、销售额、退款数及退款金额
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_order_stats(
    p_start_time TIMESTAMPTZ,
    p_end_time TIMESTAMPTZ
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_order_count BIGINT;
    v_total_amount DECIMAL(12,2);
    v_refund_count BIGINT;
    v_refund_amount DECIMAL(12,2);
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 统计订单汇总(不含已取消)
    SELECT 
        COUNT(*), 
        COALESCE(SUM(total_amount), 0)
    INTO v_order_count, v_total_amount
    FROM public.ml_orders
    WHERE created_at >= p_start_time 
      AND created_at <= p_end_time
      AND order_status != 5; -- 5: 已取消

    -- 3. 统计退款汇总
    -- 注意:这里基于 ml_orders 的 payment_status 或 order_status 判断已退款
    SELECT 
        COUNT(*), 
        COALESCE(SUM(discount_amount), 0) -- 暂时用这个,若有真实退款金额字段请替换
    INTO v_refund_count, v_refund_amount
    FROM public.ml_orders
    WHERE created_at >= p_start_time 
      AND created_at <= p_end_time
      AND order_status IN (6, 7); -- 6: 退款中, 7: 已退款

    RETURN jsonb_build_object(
        'order_count', v_order_count,
        'total_amount', v_total_amount,
        'refund_count', v_refund_count,
        'refund_amount', v_refund_amount
    );
END;
$$;
-- =====================================================================================
-- Admin 订单统计 - 趋势统计 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按天聚合指定时间范围内的订单量/销售额/退款量/退款金额
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_order_trend(
    p_start_time TIMESTAMPTZ,
    p_end_time TIMESTAMPTZ,
    p_group_by TEXT DEFAULT 'day'
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 目前仅支持 day
    IF p_group_by IS NULL OR p_group_by != 'day' THEN
        RAISE EXCEPTION 'Unsupported group_by';
    END IF;

    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT
            to_char(date_trunc('day', o.created_at), 'YYYY-MM-DD') AS date_group,
            COUNT(*) FILTER (WHERE o.order_status != 5) AS order_count,
            COALESCE(SUM(o.total_amount) FILTER (WHERE o.order_status != 5), 0) AS total_amount,
            COUNT(*) FILTER (WHERE o.order_status IN (6, 7)) AS refund_count,
            COALESCE(SUM(o.discount_amount) FILTER (WHERE o.order_status IN (6, 7)), 0) AS refund_amount
        FROM public.ml_orders o
        WHERE o.created_at >= p_start_time
          AND o.created_at <= p_end_time
        GROUP BY date_trunc('day', o.created_at)
        ORDER BY date_trunc('day', o.created_at) ASC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- Admin 订单统计 - 订单类型分布统计 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按订单类型(普通、收银、核销)统计指定时间段内的销售额及其占比
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_order_type_stats(
    p_start_time TIMESTAMPTZ,
    p_end_time TIMESTAMPTZ
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_total_amount DECIMAL(12,2);
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 计算总销售额(用于算占比)
    SELECT COALESCE(SUM(total_amount), 0) INTO v_total_amount
    FROM public.ml_orders
    WHERE created_at >= p_start_time AND created_at <= p_end_time
      AND order_status != 5; -- 排除已取消

    -- 3. 按类型统计
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT
            CASE o.order_type
                WHEN 1 THEN '普通订单'
                WHEN 2 THEN '收银订单'
                WHEN 3 THEN '核销订单'
                ELSE '其他类型'
            END AS name,
            COALESCE(SUM(o.total_amount), 0) AS amount,
            CASE 
                WHEN v_total_amount > 0 THEN ROUND((COALESCE(SUM(o.total_amount), 0) / v_total_amount * 100), 2)
                ELSE 0 
            END AS rate
        FROM public.ml_orders o
        WHERE o.created_at >= p_start_time AND o.created_at <= p_end_time
          AND o.order_status != 5
        GROUP BY o.order_type
        ORDER BY amount DESC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- Admin 订单功能 - 售后退款列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_refund_order_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_refund_status INTEGER DEFAULT NULL,
    p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ml_refund_orders ro
    WHERE (p_refund_status IS NULL OR ro.refund_status = p_refund_status)
      AND (p_search IS NULL OR (
          ro.refund_no ILIKE '%' || p_search || '%' OR
          EXISTS (
              SELECT 1 FROM public.ml_orders o 
              WHERE o.id = ro.order_id AND o.order_no ILIKE '%' || p_search || '%'
          )
      ));

    -- 3. 获取明细数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            ro.id,
            ro.refund_no,
            ro.refund_amount,
            ro.refund_status,
            ro.refund_reason,
            ro.applied_at,
            o.order_no,
            o.order_status,
            u.username as customer_name,
            u.phone as customer_phone,
            (
                SELECT jsonb_build_object(
                    'product_name', oi.product_name,
                    'image_url', oi.image_url
                )
                FROM public.ml_order_items oi
                WHERE oi.order_id = ro.order_id
                LIMIT 1
            ) as product_summary
        FROM public.ml_refund_orders ro
        LEFT JOIN public.ml_orders o ON ro.order_id = o.id
        LEFT JOIN public.ak_users u ON ro.user_id = u.id
        WHERE (p_refund_status IS NULL OR ro.refund_status = p_refund_status)
          AND (p_search IS NULL OR (
              ro.refund_no ILIKE '%' || p_search || '%' OR
              o.order_no ILIKE '%' || p_search || '%'
          ))
        ORDER BY ro.applied_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;-- =====================================================================================
-- Admin 订单功能 - 核销记录列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_write_off_record_list(
    p_page INTEGER DEFAULT 1,
    p_page_size INTEGER DEFAULT 15,
    p_search TEXT DEFAULT NULL,
    p_verified_only BOOLEAN DEFAULT TRUE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_offset INTEGER;
    v_total BIGINT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    v_offset := (p_page - 1) * p_page_size;

    -- 2. 获取总数(核销订单类型 = 3
    SELECT COUNT(*) INTO v_total
    FROM public.ml_orders o
    WHERE o.order_type = 3
      AND (p_verified_only = FALSE OR o.verified_at IS NOT NULL)
      AND (p_search IS NULL OR o.order_no ILIKE '%' || p_search || '%');

    -- 3. 获取明细
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            o.id,
            o.order_no,
            o.total_amount,
            o.payment_status,
            o.order_status,
            o.created_at,
            o.verified_at,
            buyer.username as customer_name,
            buyer.phone as customer_phone,
            verifier.username as verifier_name,
            (
                SELECT jsonb_build_object(
                    'product_name', oi.product_name,
                    'image_url', oi.image_url
                )
                FROM public.ml_order_items oi
                WHERE oi.order_id = o.id
                LIMIT 1
            ) as product_summary
        FROM public.ml_orders o
        LEFT JOIN public.ak_users buyer ON o.user_id = buyer.id
        LEFT JOIN public.ak_users verifier ON o.verifier_id = verifier.id
        WHERE o.order_type = 3
          AND (p_verified_only = FALSE OR o.verified_at IS NOT NULL)
          AND (p_search IS NULL OR o.order_no ILIKE '%' || p_search || '%')
        ORDER BY o.verified_at DESC NULLS LAST, o.created_at DESC
        LIMIT p_page_size
        OFFSET v_offset
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;-- =====================================================================================
-- Admin 商品模块 - 删除分类 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1支持级联软删除商品关联
-- 依赖ml_categories, ml_products, ak_users 表已存在
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_category_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('product:category:delete') THEN
        RAISE EXCEPTION 'Permission denied: product:category:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 检查是否有子分类 (方案 1)
    IF EXISTS (
        SELECT 1 FROM public.ml_categories 
        WHERE parent_id = p_id AND deleted_at IS NULL
    ) THEN
        RAISE EXCEPTION '请先删除该分类下的子分类';
    END IF;

    -- 4. 级联软删除:该分类下的所有商品
    UPDATE public.ml_products
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE category_id = p_id AND deleted_at IS NULL;

    -- 5. 执行软删除分类本身
    UPDATE public.ml_categories
    SET deleted_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    RETURN FOUND;
END;
$$;
-- RPC: rpc_admin_get_product_reviews
-- 作用:管理端分页获取商品评论列表,包含商品名称、用户名及规格
-- 位置docs/sql/30_rpc/product/rpc_admin_get_product_reviews_v1.sql

CREATE OR REPLACE FUNCTION public.rpc_admin_get_product_reviews(
    p_search_product text DEFAULT NULL,
    p_search_user text DEFAULT NULL,
    p_status integer DEFAULT NULL,
    p_start_time timestamptz DEFAULT NULL,
    p_end_time timestamptz DEFAULT NULL,
    p_page integer DEFAULT 1,
    p_page_size integer DEFAULT 20
)
RETURNS TABLE (
    id uuid,
    product_id uuid,
    product_name text,
    product_image text,
    user_id uuid,
    username text,
    rating integer,
    content text,
    merchant_reply text,
    status integer,
    created_at timestamptz,
    total_count bigint
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_offset integer := (p_page - 1) * p_page_size;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE ak_users.id = auth.uid() AND ak_users.role = 'admin'
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    RETURN QUERY
    WITH filtered_reviews AS (
        SELECT 
            r.*,
            p.name as p_name,
            p.main_image_url as p_image,
            u.username as u_name,
            COUNT(*) OVER() as full_count
        FROM public.ml_product_reviews r
        LEFT JOIN public.ml_products p ON r.product_id = p.id
        LEFT JOIN public.ak_users u ON r.user_id = u.id
        WHERE (p_search_product IS NULL OR p.name ILIKE '%' || p_search_product || '%')
          AND (p_search_user IS NULL OR u.username ILIKE '%' || p_search_user || '%')
          AND (p_status IS NULL OR r.status = p_status)
          AND (p_start_time IS NULL OR r.created_at >= p_start_time)
          AND (p_end_time IS NULL OR r.created_at <= p_end_time)
    )
    SELECT 
        fr.id,
        fr.product_id,
        fr.p_name as product_name,
        fr.p_image as product_image,
        fr.user_id,
        fr.u_name as username,
        fr.rating,
        fr.content,
        fr.merchant_reply,
        fr.status,
        fr.created_at,
        fr.full_count as total_count
    FROM filtered_reviews fr
    ORDER BY fr.created_at DESC
    LIMIT p_page_size OFFSET v_offset;
END;
$$;

-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_product_reviews(text, text, integer, timestamptz, timestamptz, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_product_reviews(text, text, integer, timestamptz, timestamptz, integer, integer) TO authenticated;
-- =====================================================================================
-- Admin 商品模块 - 商品统计概况 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_products, ml_orders, ml_browse_history, ak_users
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_product_stats(
    p_start_time TIMESTAMP WITH TIME ZONE,
    p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_stats JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 统计核心指标
    -- 商品浏览量 (PV), 访客数 (UV), 支付件数, 支付金额, 退款件数, 退款金额
    WITH stats AS (
        SELECT
            (SELECT COALESCE(SUM(browse_duration), 0) FROM public.ml_browse_history WHERE created_at BETWEEN p_start_time AND p_end_time) as total_views,
            (SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history WHERE created_at BETWEEN p_start_time AND p_end_time) as total_visitors,
            (SELECT COALESCE(SUM(quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id 
             WHERE o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_count,
            (SELECT COALESCE(SUM(paid_amount), 0) FROM public.ml_orders 
             WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status NOT IN (1, 5)) as pay_amount,
            (SELECT COUNT(*) FROM public.ml_orders WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status = 7) as refund_count,
            (SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status = 7) as refund_amount
    )
    SELECT jsonb_build_object(
        'views', total_views,
        'visitors', total_visitors,
        'pay_count', pay_count,
        'pay_amount', pay_amount,
        'refund_count', refund_count,
        'refund_amount', refund_amount
    ) INTO v_stats FROM stats;

    RETURN v_stats; 
END;
$$;

-- =====================================================================================
-- Admin 商品模块 - 商品排行 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_product_ranking(
    p_start_time TIMESTAMP WITH TIME ZONE,
    p_end_time TIMESTAMP WITH TIME ZONE,
    p_sort_by TEXT DEFAULT 'sales', -- views, sales, amount
    p_limit INTEGER DEFAULT 10
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取排行数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            p.id,
            p.name,
            p.main_image_url as image,
            COALESCE(p.view_count, 0) as views,
            (SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history bh WHERE bh.product_id = p.id AND bh.created_at BETWEEN p_start_time AND p_end_time) as visitors,
            (SELECT COALESCE(SUM(quantity), 0) FROM public.ml_shopping_cart sc WHERE sc.product_id = p.id AND sc.created_at BETWEEN p_start_time AND p_end_time) as cart_count,
            (SELECT COUNT(DISTINCT o.id) FROM public.ml_orders o JOIN public.ml_order_items oi ON o.id = oi.order_id 
             WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time) as order_count,
            (SELECT COALESCE(SUM(oi.quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id 
             WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_count,
            (SELECT COALESCE(SUM(oi.total_amount), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id 
             WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_amount,
            (SELECT COUNT(*) FROM public.ml_user_favorites f WHERE f.target_id = p.id AND f.target_type = 1 AND f.created_at BETWEEN p_start_time AND p_end_time) as fav_count
        FROM public.ml_products p
        WHERE p.status != 4
        ORDER BY 
            CASE 
                WHEN p_sort_by = 'views' THEN COALESCE(p.view_count, 0)
                WHEN p_sort_by = 'sales' THEN (
                    SELECT COALESCE(SUM(oi.quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id 
                    WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)
                )
                WHEN p_sort_by = 'amount' THEN (
                    SELECT COALESCE(SUM(oi.total_amount), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id 
                    WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)
                )
                ELSE COALESCE(p.view_count, 0)
            END DESC
        LIMIT p_limit
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- Admin 商品管理 - 商品状态汇总统计 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:统计出售中、仓库中、草稿箱、回收站各状态的商品数量
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_product_count_stats()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_result JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 统计各状态数量
    -- status 定义1:上架(出售中), 2:下架(仓库中), 3:草稿, 4:逻辑删除(回收站)
    SELECT jsonb_build_object(
        'selling', COUNT(*) FILTER (WHERE status = 1),
        'warehouse', COUNT(*) FILTER (WHERE status = 2),
        'draft', COUNT(*) FILTER (WHERE status = 3),
        'recycle', COUNT(*) FILTER (WHERE status = 4)
    ) INTO v_result
    FROM public.ml_products;

    RETURN v_result;
END;
$$;
-- =====================================================================================
-- Admin 商品统计 - 营业趋势统计 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按天聚合指定时间范围内的商品浏览量、访客量、支付金额及退款金额
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_product_trend(
    p_start_time TIMESTAMP WITH TIME ZONE,
    p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 按日聚合统计
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT
            to_char(date_trunc('day', gs.day), 'YYYY-MM-DD') AS date_group,
            (SELECT COUNT(*) FROM public.ml_browse_history bh WHERE date_trunc('day', bh.created_at) = gs.day) as views,
            (SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history bh WHERE date_trunc('day', bh.created_at) = gs.day) as visitors,
            (SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders o WHERE date_trunc('day', o.created_at) = gs.day AND o.order_status NOT IN (1, 5)) as pay_amount,
            (SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders o WHERE date_trunc('day', o.created_at) = gs.day AND o.order_status = 7) as refund_amount
        FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
        ORDER BY gs.day ASC
    ) t;

    RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_group_delete
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户分组(使用通用权限校验)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('user:group:delete') THEN
        RAISE EXCEPTION 'Permission denied: user:group:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 逻辑删除
    UPDATE public.ak_user_groups
    SET deleted_at = now(), 
        updated_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_group_list
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取用户分组列表,支持搜索、状态筛选及逻辑删除过滤
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_list(
    p_page INT,
    p_page_size INT,
    p_search TEXT DEFAULT NULL,
    p_status INT DEFAULT NULL,
    p_include_deleted BOOLEAN DEFAULT FALSE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_total INT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ak_user_groups
    WHERE (p_include_deleted OR deleted_at IS NULL)
      AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%')
      AND (p_status IS NULL OR status = p_status);

    -- 3. 分页获取数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            id, name, remark, status,
            created_at, updated_at, deleted_at
        FROM public.ak_user_groups
        WHERE (p_include_deleted OR deleted_at IS NULL)
          AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%')
          AND (p_status IS NULL OR status = p_status)
        ORDER BY created_at DESC
        LIMIT p_page_size
        OFFSET (p_page - 1) * p_page_size
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_group_save
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:新增/更新用户分组(逻辑删除记录默认不允许更新)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_save(
    p_id UUID DEFAULT NULL,
    p_name TEXT,
    p_remark TEXT DEFAULT NULL,
    p_status INT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 参数校验
    IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
        RAISE EXCEPTION 'Invalid name';
    END IF;

    -- 3. 新增
    IF p_id IS NULL THEN
        INSERT INTO public.ak_user_groups(
            name, remark, status,
            created_at, updated_at, deleted_at
        ) VALUES (
            p_name, p_remark, COALESCE(p_status, 1),
            now(), now(), NULL
        )
        RETURNING id INTO v_id;

        RETURN v_id;
    END IF;

    -- 4. 更新(不允许更新已删除记录)
    UPDATE public.ak_user_groups
    SET
        name = p_name,
        remark = p_remark,
        status = COALESCE(p_status, status),
        updated_at = now()
    WHERE id = p_id AND deleted_at IS NULL
    RETURNING id INTO v_id;

    IF v_id IS NULL THEN
        RAISE EXCEPTION 'Not found or deleted';
    END IF;

    RETURN v_id;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_group_set_status
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:设置用户分组状态(启用/禁用)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_set_status(
    p_id UUID,
    p_status INT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    UPDATE public.ak_user_groups
    SET status = p_status,
        updated_at = now()
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_label_delete
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户标签(使用通用权限校验)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('user:label:delete') THEN
        RAISE EXCEPTION 'Permission denied: user:label:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 逻辑删除
    UPDATE public.ak_user_labels
    SET deleted_at = now(), 
        updated_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_label_list
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取用户标签列表,支持搜索、状态筛选及逻辑删除过滤
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_list(
    p_page INT,
    p_page_size INT,
    p_search TEXT DEFAULT NULL,
    p_status INT DEFAULT NULL,
    p_include_deleted BOOLEAN DEFAULT FALSE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_total INT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ak_user_labels
    WHERE (p_include_deleted OR deleted_at IS NULL)
      AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%')
      AND (p_status IS NULL OR status = p_status);

    -- 3. 分页获取数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            id, name, color, remark, status,
            created_at, updated_at, deleted_at
        FROM public.ak_user_labels
        WHERE (p_include_deleted OR deleted_at IS NULL)
          AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%')
          AND (p_status IS NULL OR status = p_status)
        ORDER BY created_at DESC
        LIMIT p_page_size
        OFFSET (p_page - 1) * p_page_size
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_label_save
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:新增/更新用户标签(逻辑删除记录默认不允许更新)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_save(
    p_id UUID DEFAULT NULL,
    p_name TEXT,
    p_color TEXT DEFAULT NULL,
    p_remark TEXT DEFAULT NULL,
    p_status INT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 参数校验
    IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
        RAISE EXCEPTION 'Invalid name';
    END IF;

    -- 3. 新增
    IF p_id IS NULL THEN
        INSERT INTO public.ak_user_labels(
            name, color, remark, status,
            created_at, updated_at, deleted_at
        ) VALUES (
            p_name, p_color, p_remark, COALESCE(p_status, 1),
            now(), now(), NULL
        )
        RETURNING id INTO v_id;

        RETURN v_id;
    END IF;

    -- 4. 更新(不允许更新已删除记录)
    UPDATE public.ak_user_labels
    SET
        name = p_name,
        color = p_color,
        remark = p_remark,
        status = COALESCE(p_status, status),
        updated_at = now()
    WHERE id = p_id AND deleted_at IS NULL
    RETURNING id INTO v_id;

    IF v_id IS NULL THEN
        RAISE EXCEPTION 'Not found or deleted';
    END IF;

    RETURN v_id;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_label_set_status
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:设置用户标签状态(启用/禁用)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_set_status(
    p_id UUID,
    p_status INT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    UPDATE public.ak_user_labels
    SET status = p_status,
        updated_at = now()
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_delete
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户等级(使用通用权限校验)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_delete(
    p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
    v_user_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT public.check_admin_permission('user:level:delete') THEN
        RAISE EXCEPTION 'Permission denied: user:level:delete';
    END IF;

    -- 2. 获取当前操作用户 ID
    SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();

    -- 3. 逻辑删除
    UPDATE public.ak_user_levels
    SET deleted_at = now(), 
        updated_at = now(),
        deleted_by = v_user_id
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_list
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取用户等级列表,支持搜索、状态筛选及逻辑删除过滤
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_list(
    p_page INT,
    p_page_size INT,
    p_search TEXT DEFAULT NULL,
    p_status INT DEFAULT NULL,
    p_is_visible BOOLEAN DEFAULT NULL,
    p_include_deleted BOOLEAN DEFAULT FALSE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_total INT;
    v_items JSONB;
BEGIN
    -- 1. 权限检查 (依赖 public.get_current_user_role())
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 获取总数
    SELECT COUNT(*) INTO v_total
    FROM public.ak_user_levels
    WHERE (p_include_deleted OR deleted_at IS NULL)
      AND (p_search IS NULL OR name ILIKE '%' || p_search || '%')
      AND (p_status IS NULL OR status = p_status)
      AND (p_is_visible IS NULL OR is_visible = p_is_visible);

    -- 3. 分页获取数据
    SELECT jsonb_agg(t) INTO v_items
    FROM (
        SELECT 
            id, name, level_weight, min_experience, discount_percent,
            is_visible, status, icon_url, bg_image_url, bg_style_json,
            remark, created_at, updated_at, deleted_at
        FROM public.ak_user_levels
        WHERE (p_include_deleted OR deleted_at IS NULL)
          AND (p_search IS NULL OR name ILIKE '%' || p_search || '%')
          AND (p_status IS NULL OR status = p_status)
          AND (p_is_visible IS NULL OR is_visible = p_is_visible)
        ORDER BY level_weight ASC
        LIMIT p_page_size
        OFFSET (p_page - 1) * p_page_size
    ) t;

    RETURN jsonb_build_object(
        'total', v_total,
        'items', COALESCE(v_items, '[]'::jsonb)
    );
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_save
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:新增/更新用户等级(逻辑删除记录默认不允许更新)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_save(
    p_id UUID DEFAULT NULL,
    p_name TEXT,
    p_level_weight INT,
    p_min_experience INT,
    p_discount_percent INT,
    p_is_visible BOOLEAN,
    p_status INT,
    p_icon_url TEXT DEFAULT NULL,
    p_bg_image_url TEXT DEFAULT NULL,
    p_bg_style_json JSONB DEFAULT NULL,
    p_remark TEXT DEFAULT NULL
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_id UUID;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    -- 2. 参数校验(最小化)
    IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
        RAISE EXCEPTION 'Invalid name';
    END IF;

    IF p_level_weight < 0 OR p_min_experience < 0 THEN
        RAISE EXCEPTION 'Invalid level_weight or min_experience';
    END IF;

    IF p_discount_percent < 1 OR p_discount_percent > 100 THEN
        RAISE EXCEPTION 'Invalid discount_percent';
    END IF;

    -- 3. 新增
    IF p_id IS NULL THEN
        INSERT INTO public.ak_user_levels(
            name, level_weight, min_experience, discount_percent,
            is_visible, status,
            icon_url, bg_image_url, bg_style_json,
            remark,
            created_at, updated_at, deleted_at
        ) VALUES (
            p_name, p_level_weight, p_min_experience, p_discount_percent,
            p_is_visible, p_status,
            p_icon_url, p_bg_image_url, p_bg_style_json,
            p_remark,
            now(), now(), NULL
        )
        RETURNING id INTO v_id;

        RETURN v_id;
    END IF;

    -- 4. 更新(不允许更新已删除记录)
    UPDATE public.ak_user_levels
    SET
        name = p_name,
        level_weight = p_level_weight,
        min_experience = p_min_experience,
        discount_percent = p_discount_percent,
        is_visible = p_is_visible,
        status = p_status,
        icon_url = p_icon_url,
        bg_image_url = p_bg_image_url,
        bg_style_json = p_bg_style_json,
        remark = p_remark,
        updated_at = now()
    WHERE id = p_id AND deleted_at IS NULL
    RETURNING id INTO v_id;

    IF v_id IS NULL THEN
        RAISE EXCEPTION 'Not found or deleted';
    END IF;

    RETURN v_id;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_set_status
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:设置用户等级状态(启用/禁用)
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_set_status(
    p_id UUID,
    p_status INT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    UPDATE public.ak_user_levels
    SET status = p_status,
        updated_at = now()
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_set_visible
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:设置用户等级是否展示
-- =====================================================================================

CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_set_visible(
    p_id UUID,
    p_is_visible BOOLEAN
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
    v_ok BOOLEAN;
BEGIN
    -- 1. 权限检查
    IF NOT EXISTS (
        SELECT 1 FROM public.ak_users 
        WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
    ) THEN
        RAISE EXCEPTION 'Permission denied';
    END IF;

    UPDATE public.ak_user_levels
    SET is_visible = p_is_visible,
        updated_at = now()
    WHERE id = p_id AND deleted_at IS NULL;

    GET DIAGNOSTICS v_ok = ROW_COUNT;
    RETURN v_ok;
END;
$$;

关键 RPC 清单

函数名 参数 返回 用途
rpc_admin_get_overall_stats JSONB
rpc_admin_get_system_info JSONB
rpc_admin_system_config_get p_key TEXT JSONB
rpc_admin_system_config_save p_key TEXT, p_value JSONB, p_description TEXT DEFAULT NULL BOOLEAN
rpc_analytics_user_gender_distribution p_start_date DATE, p_end_date DATE TABLE ( name TEXT, value BIGINT )
check_admin_permission p_permission_code TEXT DEFAULT NULL BOOLEAN
get_current_user_role TEXT
handle_new_user trigger
handle_new_user trigger
handle_new_user trigger
rpc_admin_delete_permission p_id UUID BOOLEAN
rpc_admin_delete_role p_id UUID BOOLEAN
rpc_admin_get_admin_list p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAU JSONB
rpc_admin_get_permission_list JSONB
rpc_admin_get_role_list p_search TEXT DEFAULT NULL, p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAUL JSONB
rpc_admin_save_permission p_id UUID DEFAULT NULL, p_parent_id UUID DEFAULT NULL, p_name TEXT DEFAULT NULL, UUID
rpc_admin_save_role p_id UUID DEFAULT NULL, p_name TEXT DEFAULT NULL, p_code TEXT DEFAULT NULL, p_de UUID
rpc_admin_article_category_delete p_id UUID BOOLEAN
rpc_admin_article_category_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_search TEXT DEFAULT JSONB
rpc_admin_article_category_save p_id UUID DEFAULT NULL, p_name TEXT DEFAULT NULL, p_icon TEXT DEFAULT NULL, p_so UUID
rpc_admin_article_category_set_status p_id UUID, p_status SMALLINT BOOLEAN
rpc_admin_article_delete p_id UUID BOOLEAN
rpc_admin_article_get_detail p_id UUID JSONB
rpc_admin_article_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_category_id UUID DEF JSONB
rpc_admin_article_save p_id UUID DEFAULT NULL, p_category_id UUID DEFAULT NULL, p_title TEXT DEFAULT NU UUID
rpc_admin_article_set_status p_id UUID, p_status SMALLINT BOOLEAN
rpc_admin_delete_diy_page p_id uuid boolean
rpc_admin_get_diy_page_list p_search text DEFAULT NULL, p_type text DEFAULT NULL, p_page integer DEFAULT 1, JSONB
rpc_admin_save_diy_page p_id uuid DEFAULT NULL, p_name text DEFAULT NULL, p_type text DEFAULT NULL, p_co uuid
rpc_admin_set_home_page p_id uuid boolean
rpc_admin_delete_delivery_staff p_id UUID BOOLEAN
rpc_admin_delete_delivery_station p_id UUID BOOLEAN
rpc_admin_get_delivery_staff_list p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAU JSONB
rpc_admin_get_delivery_staff_list p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAU JSONB
rpc_admin_get_delivery_station_list p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAU JSONB
rpc_admin_save_delivery_staff p_id UUID DEFAULT NULL, p_nickname TEXT DEFAULT NULL, p_avatar TEXT DEFAULT NULL UUID
rpc_admin_save_delivery_staff p_id UUID DEFAULT NULL, p_uid UUID DEFAULT NULL, p_station_id UUID DEFAULT NULL, UUID
rpc_admin_save_delivery_station p_id UUID DEFAULT NULL, p_name TEXT DEFAULT NULL, p_phone TEXT DEFAULT NULL, p_a UUID
rpc_homecare_dispatch_candidate p_service_code TEXT DEFAULT NULL, p_station_id UUID DEFAULT NULL JSONB
rpc_admin_delete_agent p_uid uuid boolean
rpc_admin_delete_division p_uid uuid boolean
rpc_admin_get_agent_apply_list p_status text DEFAULT 'all', p_search text DEFAULT NULL, p_page integer DEFAULT TABLE ( id uuid, uid uuid, name text, phone text, dept_uid uuid, dept_name text,
rpc_admin_get_agent_list p_search text DEFAULT NULL, p_page integer DEFAULT 1, p_page_size integer DEFAUL TABLE ( uid uuid, name text, division_uid uuid, division_name text, commission_r
rpc_admin_get_division_list p_search text DEFAULT NULL, p_page integer DEFAULT 1, p_page_size integer DEFAUL TABLE ( uid uuid, name text, invite_code text, commission_ratio numeric, is_enab
rpc_admin_get_promoter_list p_search text DEFAULT NULL, p_page integer DEFAULT 1, p_page_size integer DEFAUL TABLE ( id uuid, nickname text, name text, phone text, avatar_url text, level te
rpc_admin_process_agent_apply p_id uuid, p_status text, -- approved / rejected p_refusal_reason text DEFAULT N boolean
rpc_admin_save_agent p_uid uuid, p_division_uid uuid, p_name text, p_commission_ratio numeric, p_is_e uuid
rpc_admin_save_division p_uid uuid, p_name text, p_invite_code text, p_commission_ratio numeric, p_is_en uuid
rpc_admin_balance_distribution p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE JSONB
rpc_admin_balance_stats JSONB
rpc_admin_balance_trend p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE JSONB
rpc_admin_extract_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_status SMALLINT DEFA JSONB
rpc_admin_extract_review p_extract_id UUID, p_status SMALLINT, -- 1: 通过, -1: 驳回 p_refusal_reason TEXT DEF VOID
rpc_admin_finance_bill_summary p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE, p_in JSONB
rpc_admin_finance_overview p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE JSONB
rpc_admin_invoice_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_status SMALLINT DEFA JSONB
rpc_admin_invoice_process p_id UUID, p_status SMALLINT, -- 1: 已开票, -1: 已拒绝 p_invoice_url TEXT DEFAULT NULL BOOLEAN
rpc_admin_recharge_audit p_recharge_id UUID, p_mark TEXT DEFAULT '管理员人工审计/补单' VOID
rpc_admin_recharge_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_paid SMALLINT DEFAUL JSONB
rpc_admin_user_bill_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_category VARCHAR DEF JSONB
rpc_admin_kefu_account_delete p_id UUID BOOLEAN
rpc_admin_kefu_account_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_search TEXT DEFAULT JSONB
rpc_admin_kefu_account_save p_id UUID DEFAULT NULL, p_user_id UUID DEFAULT NULL, p_nickname TEXT DEFAULT NUL UUID
rpc_admin_kefu_account_set_status p_id UUID, p_status SMALLINT BOOLEAN
rpc_admin_kefu_auto_reply_delete p_id UUID BOOLEAN
rpc_admin_kefu_auto_reply_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_search TEXT DEFAULT JSONB
rpc_admin_kefu_auto_reply_save p_id UUID DEFAULT NULL, p_keyword TEXT DEFAULT NULL, p_content TEXT DEFAULT NULL UUID
rpc_admin_kefu_auto_reply_set_status p_id UUID, p_status SMALLINT BOOLEAN
rpc_admin_kefu_feedback_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_search TEXT DEFAULT JSONB
rpc_admin_kefu_feedback_process p_id UUID, p_reply_content TEXT BOOLEAN
rpc_admin_kefu_word_category_delete p_id UUID BOOLEAN
rpc_admin_kefu_word_category_list JSONB
rpc_admin_kefu_word_category_save p_id UUID DEFAULT NULL, p_name TEXT DEFAULT NULL, p_sort INTEGER DEFAULT 0 UUID
rpc_admin_kefu_word_delete p_id UUID BOOLEAN
rpc_admin_kefu_word_list p_category_id UUID DEFAULT NULL, p_search TEXT DEFAULT NULL JSONB
rpc_admin_kefu_word_save p_id UUID DEFAULT NULL, p_category_id UUID DEFAULT NULL, p_title TEXT DEFAULT NU UUID
rpc_admin_get_integral_stats p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE JSONB
rpc_admin_cashier_order_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_search_order_no TEXT JSONB
rpc_admin_order_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_order_status INTEGER JSONB
rpc_admin_order_source_stats p_start_time TIMESTAMPTZ, p_end_time TIMESTAMPTZ JSONB
rpc_admin_order_stats p_start_time TIMESTAMPTZ, p_end_time TIMESTAMPTZ JSONB
rpc_admin_order_trend p_start_time TIMESTAMPTZ, p_end_time TIMESTAMPTZ, p_group_by TEXT DEFAULT 'day' JSONB
rpc_admin_order_type_stats p_start_time TIMESTAMPTZ, p_end_time TIMESTAMPTZ JSONB
rpc_admin_refund_order_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_refund_status INTEGE JSONB
rpc_admin_write_off_record_list p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_search TEXT DEFAULT JSONB
rpc_admin_category_delete p_id UUID BOOLEAN
rpc_admin_get_product_reviews p_search_product text DEFAULT NULL, p_search_user text DEFAULT NULL, p_status in TABLE ( id uuid, product_id uuid, product_name text, product_image text, user_id
rpc_admin_product_stats p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE JSONB
rpc_admin_product_ranking p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE, p_so JSONB
rpc_admin_product_count_stats JSONB
rpc_admin_product_trend p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE JSONB
rpc_admin_user_group_delete p_id UUID BOOLEAN
rpc_admin_user_group_list p_page INT, p_page_size INT, p_search TEXT DEFAULT NULL, p_status INT DEFAULT NU JSONB
rpc_admin_user_group_save p_id UUID DEFAULT NULL, p_name TEXT, p_remark TEXT DEFAULT NULL, p_status INT DE UUID
rpc_admin_user_group_set_status p_id UUID, p_status INT BOOLEAN
rpc_admin_user_label_delete p_id UUID BOOLEAN
rpc_admin_user_label_list p_page INT, p_page_size INT, p_search TEXT DEFAULT NULL, p_status INT DEFAULT NU JSONB
rpc_admin_user_label_save p_id UUID DEFAULT NULL, p_name TEXT, p_color TEXT DEFAULT NULL, p_remark TEXT DE UUID
rpc_admin_user_label_set_status p_id UUID, p_status INT BOOLEAN
rpc_admin_user_level_delete p_id UUID BOOLEAN
rpc_admin_user_level_list p_page INT, p_page_size INT, p_search TEXT DEFAULT NULL, p_status INT DEFAULT NU JSONB
rpc_admin_user_level_save p_id UUID DEFAULT NULL, p_name TEXT, p_level_weight INT, p_min_experience INT, p UUID
rpc_admin_user_level_set_status p_id UUID, p_status INT BOOLEAN
rpc_admin_user_level_set_visible p_id UUID, p_is_visible BOOLEAN BOOLEAN