442 KiB
医疗-consumer & 医疗-delivery 数据库对接文档
生成日期:2026-06-01 适用范围:医疗-consumer(消费者端)+ 医疗-delivery(配送端) 数据库:PostgreSQL (Supabase) 规范:所有用户侧表已启用 RLS(行级安全),全局/管理后台查询请走 RPC(SECURITY 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_users1:1 关系(user_idUNIQUE)
状态机速查
| 状态域 | 字段名 | 关键取值 | 说明 |
|---|---|---|---|
| 订单流程 | 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) -> UUIDrpc_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...) -> UUIDrpc_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...) -> UUIDrpc_admin_user_level_set_status(p_id UUID, p_status INT) -> BOOLEANrpc_admin_user_level_set_visible(p_id UUID, p_is_visible BOOLEAN) -> BOOLEAN
ak_users
- 所属端:consumer / delivery
| 字段名 | 数据类型 | 约束/默认值 | 说明 |
|---|---|---|---|
| id | uuid | primary key | |
| username | text | ||
| 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) -> JSONBrpc_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_selectdelivery_staff_self_update
相关 RPC(最多展示 8 个):
rpc_admin_delete_delivery_staff(p_id UUID) -> BOOLEANrpc_admin_get_delivery_staff_list(p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAULT 1, p_page_size IN...) -> JSONBrpc_admin_get_delivery_staff_list(p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAULT 1, p_page_size IN...) -> JSONBrpc_admin_save_delivery_staff(p_id UUID DEFAULT NULL, p_nickname TEXT DEFAULT NULL, p_avatar TEXT DEFAULT NULL, p_phone TEXT DEFAU...) -> UUIDrpc_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_activedelivery_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_policyml_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...) -> JSONBrpc_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, -- 企业税号 | |
| 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...) -> UUIDrpc_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...) -> UUIDrpc_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_insertml_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, -- 主播昵称 | |
| 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) 核心商城 Schema(complete_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) 扩展模块 Schema(docs/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 |