# 医疗-consumer & 医疗-delivery 数据库对接文档 > **生成日期**:2026-06-01 > **适用范围**:医疗-consumer(消费者端)+ 医疗-delivery(配送端) > **数据库**:PostgreSQL (Supabase) > **规范**:所有用户侧表已启用 RLS(行级安全),全局/管理后台查询请走 RPC(SECURITY DEFINER) ## 目录 - [通用规范说明](#通用规范说明) - [用户与权限](#用户权限) - [商品与类目](#商品类目) - [订单](#订单) - [配送与物流](#配送物流) - [营销与促销](#营销促销) - [分销与推广](#分销推广) - [财务](#财务) - [内容与装修](#内容装修) - [客服](#客服) - [系统配置](#系统配置) - [其他](#其他) - [完整对接 SQL 汇总](#完整对接-sql-汇总) - [关键 RPC 清单](#关键-rpc-清单) ## 通用规范说明 ### 软删除标准 本项目所有业务表默认采用**软删除**,标准字段如下: - `deleted_at` (timestamptz):删除时间,NULL 表示未删除 - `deleted_by` (uuid):删除操作人 - `created_at` (timestamptz):创建时间,默认 `now()` - `updated_at` (timestamptz):更新时间,默认 `now()` > **注意**:RLS 策略默认过滤 `deleted_at IS NULL` 的行,查询时无需手动加条件,但管理后台统计需视情况处理。 ### 角色字段权威口径 - 统一用户主表:`public.ak_users` - 角色唯一权威字段:`ak_users.role`,取值:`customer`(消费者)、`merchant`(商家)、`delivery`(配送员)、`admin`(管理员)、`analytics`(数据分析师) - 商城用户扩展档案:`public.ml_user_profiles`,与 `ak_users` 1:1 关系(`user_id` UNIQUE) ### 状态机速查 | 状态域 | 字段名 | 关键取值 | 说明 | |--------|--------|----------|------| | 订单流程 | `order_status` | 1=待付款, 2=待发货, 3=待收货, 4=已完成, 5=已取消, 6=退款中, 7=已退款 | 主状态 | | 支付状态 | `payment_status` | 1=未支付, 2=已支付, 3=部分退款, 4=全额退款 | 支付线 | | 物流状态 | `shipping_status` | 1=未发货, 2=已发货, 3=运输中, 4=已送达 | 物流线 | | 配送任务 | `status` | 1=待接单, 2=已接单, 3=取货中, 4=配送中, 5=已送达, 6=配送失败 | 配送端 | ## 用户与权限 ### ml_user_profiles - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | user_id | UUID | UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | status | INTEGER | DEFAULT 1 NOT NULL, -- 1:正常 2:冻结 3:注销 4:待审核 | | | real_name | VARCHAR(100) | , -- 真实姓名 | | | id_card | VARCHAR(32) | , -- 身份证号 | | | business_license | VARCHAR(100) | , -- 营业执照号 | | | credit_score | INTEGER | DEFAULT 100, -- 信用分数 0-1000 | | | verification_status | INTEGER | DEFAULT 0, -- 认证状态0:未认证1:已认证2:认证失败 | | | verification_data | JSONB | DEFAULT '{}', -- 认证相关数据 | | | preferences | JSONB | DEFAULT '{}', -- 用户偏好设置 | | | emergency_contact | VARCHAR(200) | , -- 紧急联系人 | | | service_areas | JSONB | , -- 服务区域(配送员) | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_user_status CHECK (status IN (1,2,3,4))` - `CONSTRAINT chk_ml_verification_status CHECK (verification_status IN (0,1,2))` - `CONSTRAINT chk_ml_credit_score CHECK (credit_score >= 0 AND credit_score <= 1000)` --- ### ml_user_addresses - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | receiver_name | VARCHAR(100) | NOT NULL | | | receiver_phone | VARCHAR(32) | NOT NULL | | | province | VARCHAR(100) | NOT NULL | | | city | VARCHAR(100) | NOT NULL | | | district | VARCHAR(100) | NOT NULL | | | street | VARCHAR(200) | | | | address_detail | TEXT | NOT NULL | | | postal_code | VARCHAR(16) | | | | is_default | BOOLEAN | DEFAULT FALSE | | | label | VARCHAR(50) | , -- home/office/school/other | | | latitude | DECIMAL(10,7) | | | | longitude | DECIMAL(10,7) | | | | delivery_instructions | TEXT | | | | business_hours | VARCHAR(100) | | | | status | INTEGER | DEFAULT 1, -- 1:正常 2:禁用 | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_address_status CHECK (status IN (1,2))` --- ### ml_shopping_cart - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | sku_id | UUID | REFERENCES public.ml_product_skus(id) ON DELETE CASCADE | | | quantity | INTEGER | NOT NULL CHECK (quantity > 0) | | | selected | BOOLEAN | DEFAULT TRUE | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `UNIQUE(user_id, product_id, sku_id)` --- ### ml_user_favorites - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | target_type | INTEGER | NOT NULL, -- 1:商品 2:店铺 | | | target_id | UUID | NOT NULL | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `UNIQUE(user_id, target_type, target_id)` - `CONSTRAINT chk_ml_favorite_type CHECK (target_type IN (1,2))` --- ### ml_browse_history - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | browse_duration | INTEGER | DEFAULT 0, -- 浏览时长(秒) | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `UNIQUE(user_id, product_id)` --- ### ml_search_history - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | user_id | UUID | REFERENCES public.ak_users(id) ON DELETE CASCADE | | | keyword | VARCHAR(200) | NOT NULL | | | result_count | INTEGER | DEFAULT 0 | | | ip_address | INET | | | | user_agent | TEXT | | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | --- ### ak_roles - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL UNIQUE, -- 角色名称 (如: 超级管理员) | | | code | TEXT | NOT NULL UNIQUE, -- 角色编码 (如: super_admin) | | | description | TEXT | , -- 角色描述 | | | is_active | BOOLEAN | NOT NULL DEFAULT TRUE | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS**:已启用,暂无显式策略(默认拒绝直接访问) --- ### ak_permissions - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | parent_id | UUID | REFERENCES public.ak_permissions(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL, -- 权限/菜单名称 | | | code | TEXT | NOT NULL UNIQUE, -- 权限编码 (如: order_view) | | | type | TEXT | NOT NULL, -- 类型: menu(菜单), button(按钮/接口) | | | path | TEXT | , -- 前端路由路径 (仅针对 menu) | | | icon | TEXT | , -- 图标 | | | sort_order | INTEGER | DEFAULT 0, -- 排序 | | | is_visible | BOOLEAN | DEFAULT TRUE, -- 菜单是否在左侧可见 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS**:已启用,暂无显式策略(默认拒绝直接访问) --- ### ak_admin_roles - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | role_id | UUID | NOT NULL REFERENCES public.ak_roles(id) ON DELETE CASCADE | | | assigned_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | assigned_by | UUID | REFERENCES public.ak_users(id) | | **约束**: - `UNIQUE(user_id, role_id)` **RLS**:已启用,暂无显式策略(默认拒绝直接访问) --- ### ak_role_permissions - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | role_id | UUID | NOT NULL REFERENCES public.ak_roles(id) ON DELETE CASCADE | | | permission_id | UUID | NOT NULL REFERENCES public.ak_permissions(id) ON DELETE CASCADE | | **约束**: - `UNIQUE(role_id, permission_id)` **RLS**:已启用,暂无显式策略(默认拒绝直接访问) --- ### ak_user_groups - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL | | | remark | TEXT | NULL | | | status | INT | NOT NULL DEFAULT 1, -- 1:启用, 0:禁用 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | deleted_at | TIMESTAMPTZ | NULL | | **约束**: - `CONSTRAINT ak_user_groups_name_length CHECK (char_length(name) >= 1)` **RLS**:已启用,暂无显式策略(默认拒绝直接访问) **相关 RPC**(最多展示 8 个): - `rpc_admin_user_group_save(p_id UUID DEFAULT NULL, p_name TEXT, p_remark TEXT DEFAULT NULL, p_status INT DEFAULT 1) -> UUID` - `rpc_admin_user_group_set_status(p_id UUID, p_status INT) -> BOOLEAN` --- ### ak_user_labels - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL | | | color | TEXT | NULL | | | remark | TEXT | NULL | | | status | INT | NOT NULL DEFAULT 1 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | deleted_at | TIMESTAMPTZ | NULL | | **约束**: - `CONSTRAINT ak_user_labels_name_length CHECK (char_length(name) >= 1)` **RLS**:已启用,暂无显式策略(默认拒绝直接访问) **相关 RPC**(最多展示 8 个): - `rpc_admin_user_label_save(p_id UUID DEFAULT NULL, p_name TEXT, p_color TEXT DEFAULT NULL, p_remark TEXT DEFAULT NULL, p_status...) -> UUID` - `rpc_admin_user_label_set_status(p_id UUID, p_status INT) -> BOOLEAN` --- ### ak_user_levels - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL | | | level_weight | INT | NOT NULL | | | min_experience | INT | NOT NULL DEFAULT 0 | | | discount_percent | INT | NOT NULL DEFAULT 100 | | | is_visible | BOOLEAN | NOT NULL DEFAULT TRUE | | | status | INT | NOT NULL DEFAULT 1 | | | icon_url | TEXT | NULL | | | bg_image_url | TEXT | NULL | | | bg_style_json | JSONB | NULL | | | remark | TEXT | NULL | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | deleted_at | TIMESTAMPTZ | NULL | | **约束**: - `CONSTRAINT ak_user_levels_level_weight_nonnegative CHECK (level_weight >= 0)` - `CONSTRAINT ak_user_levels_min_experience_nonnegative CHECK (min_experience >= 0)` - `CONSTRAINT ak_user_levels_discount_percent_range CHECK (discount_percent BETWEEN 1 AND 100)` **RLS 策略**: - `ak_user_levels_public_select_visible_active` **相关 RPC**(最多展示 8 个): - `rpc_admin_user_level_save(p_id UUID DEFAULT NULL, p_name TEXT, p_level_weight INT, p_min_experience INT, p_discount_percent IN...) -> UUID` - `rpc_admin_user_level_set_status(p_id UUID, p_status INT) -> BOOLEAN` - `rpc_admin_user_level_set_visible(p_id UUID, p_is_visible BOOLEAN) -> BOOLEAN` --- ### ak_users - **所属端**:consumer / delivery | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | uuid | primary key | | | username | text | | | | email | text | | | | gender | text | | | | birthday | date | | | | height_cm | numeric | | | | weight_kg | numeric | | | | bio | text | | | | avatar_url | text | | | | preferred_language | text | | | | health_goal | text | | | | service_address | text | | | | emergency_contact | text | | | | chronic_notes | text | | | | care_preference | text | | | | role | text | | | | school_id | text | | | | grade_id | text | | | | class_id | text | | | | created_at | timestamptz | default now() | | | updated_at | timestamptz | default now() | | --- ## 商品与类目 ### ml_categories - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | cid | SERIAL | UNIQUE NOT NULL, -- SEO友好的自增ID | | | parent_id | UUID | REFERENCES public.ml_categories(id) | | | name | VARCHAR(200) | NOT NULL | | | slug | VARCHAR(200) | UNIQUE | | | description | TEXT | | | | icon_url | TEXT | | | | banner_url | TEXT | | | | sort_order | INTEGER | DEFAULT 0 | | | level | INTEGER | DEFAULT 1 | | | path | TEXT[] | , -- 分类路径 | | | is_active | BOOLEAN | DEFAULT TRUE | | | seo_title | VARCHAR(200) | | | | seo_description | VARCHAR(500) | | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | --- ### ml_brands - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | cid | SERIAL | UNIQUE NOT NULL, -- SEO友好的自增ID | | | name | VARCHAR(200) | NOT NULL | | | logo_url | TEXT | | | | description | TEXT | | | | website | VARCHAR(500) | | | | is_active | BOOLEAN | DEFAULT TRUE | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | --- ### ml_products - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | cid | SERIAL | UNIQUE NOT NULL, -- SEO友好的自增ID | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) | | | category_id | UUID | NOT NULL REFERENCES public.ml_categories(id) | | | brand_id | UUID | REFERENCES public.ml_brands(id) | | | product_code | VARCHAR(100) | UNIQUE NOT NULL | | | name | VARCHAR(500) | NOT NULL | | | subtitle | VARCHAR(1000) | | | | description | TEXT | | | | main_image_url | TEXT | | | | image_urls | JSONB | DEFAULT '[]' | | | video_urls | JSONB | DEFAULT '[]' | | | base_price | DECIMAL(12,2) | NOT NULL CHECK (base_price >= 0) | | | market_price | DECIMAL(12,2) | | | | cost_price | DECIMAL(12,2) | | | | total_stock | INTEGER | DEFAULT 0 CHECK (total_stock >= 0) | | | available_stock | INTEGER | DEFAULT 0 CHECK (available_stock >= 0) | | | min_order_qty | INTEGER | DEFAULT 1 CHECK (min_order_qty > 0) | | | max_order_qty | INTEGER | | | | weight | DECIMAL(10,3) | | | | dimensions | JSONB | , -- {length, width, height} | | | status | INTEGER | DEFAULT 1, -- 1:上架 2:下架 3:草稿 4:删除 | | | is_featured | BOOLEAN | DEFAULT FALSE | | | is_new | BOOLEAN | DEFAULT FALSE | | | is_hot | BOOLEAN | DEFAULT FALSE | | | view_count | INTEGER | DEFAULT 0 | | | sale_count | INTEGER | DEFAULT 0 | | | favorite_count | INTEGER | DEFAULT 0 | | | rating_avg | DECIMAL(3,2) | DEFAULT 0.00 CHECK (rating_avg >= 0 AND rating_avg <= 5) | | | rating_count | INTEGER | DEFAULT 0 | | | seo_title | VARCHAR(200) | | | | seo_description | VARCHAR(500) | | | | seo_keywords | TEXT[] | | | | slug | VARCHAR(200) | UNIQUE | | | tags | TEXT[] | | | | attributes | JSONB | DEFAULT '{}' | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | published_at | TIMESTAMP | WITH TIME ZONE | | **约束**: - `CONSTRAINT chk_ml_product_status CHECK (status IN (1,2,3,4))` **相关 RPC**(最多展示 8 个): - `rpc_admin_product_stats(p_start_time TIMESTAMP WITH TIME ZONE, p_end_time TIMESTAMP WITH TIME ZONE) -> JSONB` --- ### ml_product_skus - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | sku_code | VARCHAR(100) | UNIQUE NOT NULL | | | specifications | JSONB | DEFAULT '{}', -- 规格组合 | | | price | DECIMAL(12,2) | NOT NULL CHECK (price >= 0) | | | market_price | DECIMAL(12,2) | | | | cost_price | DECIMAL(12,2) | | | | stock | INTEGER | DEFAULT 0 CHECK (stock >= 0) | | | warning_stock | INTEGER | DEFAULT 10, -- 库存预警 | | | image_url | TEXT | | | | weight | DECIMAL(10,3) | | | | status | INTEGER | DEFAULT 1, -- 1:正常 2:禁用 | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_sku_status CHECK (status IN (1,2))` --- ### ml_product_specs - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | spec_name | VARCHAR(100) | NOT NULL, -- 规格名称:颜色、尺寸等 | | | spec_values | JSONB | NOT NULL DEFAULT '[]', -- 规格值数组 | | | sort_order | INTEGER | DEFAULT 0 | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | --- ### ml_shops - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | cid | SERIAL | UNIQUE NOT NULL, -- SEO友好的自增ID | | | merchant_id | UUID | UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | shop_name | VARCHAR(200) | NOT NULL | | | shop_logo | TEXT | | | | shop_banner | TEXT | | | | description | TEXT | | | | business_license | VARCHAR(100) | | | | contact_name | VARCHAR(100) | | | | contact_phone | VARCHAR(32) | | | | contact_email | VARCHAR(200) | | | | address | JSONB | , -- 店铺地址信息 | | | business_hours | JSONB | , -- 营业时间 | | | status | INTEGER | DEFAULT 1, -- 1:正常 2:暂停 3:关闭 | | | product_count | INTEGER | DEFAULT 0 | | | order_count | INTEGER | DEFAULT 0 | | | rating_avg | DECIMAL(3,2) | DEFAULT 0.00 | | | rating_count | INTEGER | DEFAULT 0 | | | verified_at | TIMESTAMP | WITH TIME ZONE | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_shop_status CHECK (status IN (1,2,3))` --- ### ml_product_reviews - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | order_id | UUID | NOT NULL REFERENCES public.ml_orders(id) | | | order_item_id | UUID | NOT NULL REFERENCES public.ml_order_items(id) | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id) | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) | | | rating | INTEGER | NOT NULL CHECK (rating >= 1 AND rating <= 5) | | | content | TEXT | | | | images | JSONB | DEFAULT '[]', -- 评价图片 | | | is_anonymous | BOOLEAN | DEFAULT FALSE | | | merchant_reply | TEXT | | | | merchant_replied_at | TIMESTAMP | WITH TIME ZONE | | | status | INTEGER | DEFAULT 1, -- 1:正常 2:已删除3:已隐藏 | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_review_status CHECK (status IN (1,2,3))` **相关 RPC**(最多展示 8 个): - `rpc_admin_get_product_reviews(p_search_product text DEFAULT NULL, p_search_user text DEFAULT NULL, p_status integer DEFAULT NULL, ...) -> TABLE ( id uuid, product_id uuid, product_name text, product_image text, user_id uuid, username text, rating integer, content text, merchant_reply text, status integer, created_at timestamptz, total_count bigint )` --- ### ak_shipping_templates - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL, -- 模板名称 | | | calc_method | TEXT | DEFAULT 'piece', -- 计费方式: piece(件数), weight(重量), volume(体积) | | | is_free_shipping | BOOLEAN | DEFAULT false, -- 是否包邮 | | | sort_order | INTEGER | DEFAULT 0 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | --- ### medical_mall_categories - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | TEXT | PRIMARY KEY | | | name | TEXT | NOT NULL | | | parent_id | TEXT | NULL REFERENCES public.medical_mall_categories(id) | | | level | INTEGER | NOT NULL CHECK (level IN (1, 2)) | | | sort_order | INTEGER | NOT NULL DEFAULT 0 | | | icon | TEXT | | | | image_url | TEXT | | | | description | TEXT | | | | scene | TEXT | NOT NULL DEFAULT 'medical_mall' | | | category_type | TEXT | NOT NULL DEFAULT 'normal' | | | compliance_type | TEXT | NOT NULL DEFAULT 'normal' | | | is_active | BOOLEAN | NOT NULL DEFAULT TRUE | | | deleted_at | TIMESTAMPTZ | NULL | | | deleted_by | UUID | NULL REFERENCES public.ak_users(id) | | | restored_at | TIMESTAMPTZ | NULL | | | restored_by | UUID | NULL REFERENCES public.ak_users(id) | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | **RLS 策略**: - `medical_mall_categories_public_select_active` --- ### medical_mall_product_categories - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | category_id | TEXT | NOT NULL REFERENCES public.medical_mall_categories(id) | | | is_primary | BOOLEAN | NOT NULL DEFAULT FALSE | | | sort_order | INTEGER | NOT NULL DEFAULT 0 | | | deleted_at | TIMESTAMPTZ | NULL | | | deleted_by | UUID | NULL REFERENCES public.ak_users(id) | | | restored_at | TIMESTAMPTZ | NULL | | | restored_by | UUID | NULL REFERENCES public.ak_users(id) | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | **约束**: - `UNIQUE (product_id, category_id)` **RLS 策略**: - `medical_mall_product_categories_public_select_active` --- ## 订单 ### ml_orders - **所属端**:consumer / delivery | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | cid | SERIAL | UNIQUE NOT NULL, -- SEO友好的自增ID | | | order_no | VARCHAR(50) | UNIQUE NOT NULL | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id) | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) | | | product_amount | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 商品金额 | | | discount_amount | DECIMAL(12,2) | DEFAULT 0, -- 优惠金额 | | | shipping_fee | DECIMAL(12,2) | DEFAULT 0, -- 运费 | | | total_amount | DECIMAL(12,2) | NOT NULL, -- 总金额 | | | paid_amount | DECIMAL(12,2) | DEFAULT 0, -- 已付金额 | | | shipping_address | JSONB | NOT NULL, -- 收货地址 | | | order_status | INTEGER | DEFAULT 1, -- 1:待付款2:待发货3:待收货4:已完成5:已取消6:退款中 7:已退款 | | | payment_status | INTEGER | DEFAULT 1, -- 1:未付款2:已付款3:部分退款4:全额退款 | | | shipping_status | INTEGER | DEFAULT 1, -- 1:未发货2:已发货3:运输中4:已送达 | | | paid_at | TIMESTAMP | WITH TIME ZONE | | | shipped_at | TIMESTAMP | WITH TIME ZONE | | | delivered_at | TIMESTAMP | WITH TIME ZONE | | | completed_at | TIMESTAMP | WITH TIME ZONE | | | remark | TEXT | , -- 买家备注 | | | merchant_memo | TEXT | , -- 商家备注 | | | cancel_reason | TEXT | , -- 取消原因 | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_order_status CHECK (order_status IN (1,2,3,4,5,6,7))` - `CONSTRAINT chk_ml_payment_status CHECK (payment_status IN (1,2,3,4))` - `CONSTRAINT chk_ml_shipping_status CHECK (shipping_status IN (1,2,3,4))` **相关 RPC**(最多展示 8 个): - `rpc_admin_order_source_stats(p_start_time TIMESTAMPTZ, p_end_time TIMESTAMPTZ) -> JSONB` - `rpc_admin_order_stats(p_start_time TIMESTAMPTZ, p_end_time TIMESTAMPTZ) -> JSONB` --- ### ml_order_items - **所属端**:consumer / delivery | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | order_id | UUID | NOT NULL REFERENCES public.ml_orders(id) ON DELETE CASCADE | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) | | | sku_id | UUID | REFERENCES public.ml_product_skus(id) | | | product_name | VARCHAR(500) | NOT NULL | | | sku_name | VARCHAR(500) | | | | specifications | JSONB | DEFAULT '{}' | | | image_url | TEXT | | | | price | DECIMAL(12,2) | NOT NULL | | | quantity | INTEGER | NOT NULL CHECK (quantity > 0) | | | total_amount | DECIMAL(12,2) | NOT NULL | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | --- ## 配送与物流 ### ml_delivery_drivers - **所属端**:consumer / delivery | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | user_id | UUID | UNIQUE NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | real_name | VARCHAR(100) | NOT NULL | | | id_card | VARCHAR(32) | NOT NULL | | | driver_license | VARCHAR(50) | | | | vehicle_type | INTEGER | , -- 1:电动车2:摩托车3:汽车 | | | vehicle_number | VARCHAR(20) | | | | service_areas | JSONB | DEFAULT '[]', -- 服务区域 | | | work_status | INTEGER | DEFAULT 1, -- 1:在线 2:忙碌 3:离线 | | | current_lat | DECIMAL(10,7) | | | | current_lng | DECIMAL(10,7) | | | | rating_avg | DECIMAL(3,2) | DEFAULT 0.00 | | | rating_count | INTEGER | DEFAULT 0 | | | order_count | INTEGER | DEFAULT 0 | | | status | INTEGER | DEFAULT 1, -- 1:正常 2:暂停 3:离职 | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_driver_vehicle_type CHECK (vehicle_type IN (1,2,3))` - `CONSTRAINT chk_ml_driver_work_status CHECK (work_status IN (1,2,3))` - `CONSTRAINT chk_ml_driver_status CHECK (status IN (1,2,3))` --- ### ml_delivery_tasks - **所属端**:consumer / delivery | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | order_id | UUID | UNIQUE NOT NULL REFERENCES public.ml_orders(id) | | | driver_id | UUID | REFERENCES public.ml_delivery_drivers(id) | | | pickup_address | JSONB | NOT NULL, -- 取货地址 | | | delivery_address | JSONB | NOT NULL, -- 配送地址 | | | distance | DECIMAL(8,2) | , -- 配送距离km) | | | estimated_time | INTEGER | , -- 预计配送时间分钟) | | | delivery_fee | DECIMAL(10,2) | NOT NULL DEFAULT 0 | | | status | INTEGER | DEFAULT 1, -- 1:待接单2:已接单3:取货中4:配送中 5:已送达 6:配送失败 | | | assigned_at | TIMESTAMP | WITH TIME ZONE | | | picked_at | TIMESTAMP | WITH TIME ZONE | | | delivered_at | TIMESTAMP | WITH TIME ZONE | | | delivery_code | VARCHAR(10) | , -- 取货中 | | | remark | TEXT | | | | failure_reason | TEXT | | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_delivery_status CHECK (status IN (1,2,3,4,5,6))` --- ### ml_delivery_staff - **所属端**:consumer / delivery | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | uid | UUID | REFERENCES public.ak_users(id) ON DELETE SET NULL, -- 关联用户(可选) | | | nickname | TEXT | NOT NULL, -- 配送员名称 | | | avatar | TEXT | , -- 头像 | | | phone | TEXT | NOT NULL, -- 手机号 | | | status | SMALLINT | NOT NULL DEFAULT 1, -- 状态: 1-启用, 0-禁用 | | | is_active | BOOLEAN | NOT NULL DEFAULT TRUE | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `delivery_staff_self_select` - `delivery_staff_self_update` **相关 RPC**(最多展示 8 个): - `rpc_admin_delete_delivery_staff(p_id UUID) -> BOOLEAN` - `rpc_admin_get_delivery_staff_list(p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAULT 1, p_page_size IN...) -> JSONB` - `rpc_admin_get_delivery_staff_list(p_search TEXT DEFAULT NULL, p_status SMALLINT DEFAULT NULL, p_page INTEGER DEFAULT 1, p_page_size IN...) -> JSONB` - `rpc_admin_save_delivery_staff(p_id UUID DEFAULT NULL, p_nickname TEXT DEFAULT NULL, p_avatar TEXT DEFAULT NULL, p_phone TEXT DEFAU...) -> UUID` - `rpc_admin_save_delivery_staff(p_id UUID DEFAULT NULL, p_uid UUID DEFAULT NULL, p_station_id UUID DEFAULT NULL, p_staff_no TEXT DEF...) -> UUID` --- ### ml_delivery_stations - **所属端**:consumer / delivery | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL, -- 提货点名称 | | | phone | TEXT | NOT NULL, -- 联系电话 | | | address | TEXT | NOT NULL, -- 详细地址 | | | image | TEXT | , -- 门店图片 | | | lng | NUMERIC(10,7) | , -- 经度 | | | lat | NUMERIC(10,7) | , -- 纬度 | | | status | SMALLINT | NOT NULL DEFAULT 1, -- 状态: 1-显示, 0-隐藏 | | | sort_order | INTEGER | DEFAULT 0 | | | business_hours | JSONB | , -- 营业时间 (如: {"start": "09:00", "end": "21:00"}) | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `delivery_stations_select_active` - `delivery_stations_select_active` --- ## 营销与促销 ### ml_coupon_templates - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | cid | SERIAL | UNIQUE NOT NULL, -- SEO友好的自增ID | | | merchant_id | UUID | REFERENCES public.ak_users(id), -- NULL表示平台券 | | | name | VARCHAR(200) | NOT NULL | | | description | TEXT | | | | coupon_type | INTEGER | NOT NULL, -- 1:满减券2:折扣券3:免运费券 | | | discount_type | INTEGER | NOT NULL, -- 1:固定金额 2:百分比 | | | discount_value | DECIMAL(12,2) | NOT NULL, -- 优惠值 | | | min_order_amount | DECIMAL(12,2) | DEFAULT 0, -- 最低订单金额 | | | max_discount_amount | DECIMAL(12,2) | , -- 最大优惠金额 | | | total_quantity | INTEGER | , -- 总发放数量 | | | per_user_limit | INTEGER | DEFAULT 1, -- 每用户限领数量 | | | usage_limit | INTEGER | DEFAULT 1, -- 每张券使用次数限制 | | | applicable_products | JSONB | DEFAULT '[]', -- 适用商品ID数组 | | | applicable_categories | JSONB | DEFAULT '[]', -- 适用分类ID数组 | | | start_time | TIMESTAMP | WITH TIME ZONE NOT NULL | | | end_time | TIMESTAMP | WITH TIME ZONE NOT NULL | | | status | INTEGER | DEFAULT 1, -- 1:正常 2:暂停 3:已结束 | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **约束**: - `CONSTRAINT chk_ml_coupon_type CHECK (coupon_type IN (1,2,3))` - `CONSTRAINT chk_ml_discount_type CHECK (discount_type IN (1,2))` - `CONSTRAINT chk_ml_coupon_status CHECK (status IN (1,2,3))` **RLS 策略**: - `ml_coupon_templates_merchant_policy` - `ml_coupon_templates_select_policy` --- ## 分销与推广 ### ak_commission_logs - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | source_uid | UUID | REFERENCES public.ak_users(id) ON DELETE SET NULL | | | order_id | UUID | REFERENCES public.ml_orders(id) ON DELETE SET NULL | | | order_no | VARCHAR(50) | | | | amount | DECIMAL(12,2) | NOT NULL DEFAULT 0 | | | status | TEXT | NOT NULL DEFAULT 'frozen', -- frozen/available/withdrawn/canceled | | | frozen_until | TIMESTAMPTZ | | | | remark | TEXT | | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **约束**: - `CONSTRAINT chk_ak_commission_amount_nonneg CHECK (amount >= 0)` - `CONSTRAINT chk_ak_commission_status CHECK (status IN ('frozen','available','withdrawn','canceled'))` **RLS 策略**: - `commission_logs_select_policy` --- ### ak_distribution_agent_applications - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | division_uid | UUID | NOT NULL REFERENCES public.ak_distribution_divisions(uid) | | | agent_name | TEXT | NOT NULL | | | agent_phone | TEXT | NULL | | | proof_images | JSONB | NULL, -- 申请凭证图片列表 | | | status | TEXT | NOT NULL DEFAULT 'pending', -- pending/approved/rejected | | | refusal_reason | TEXT | NULL | | | approved_at | TIMESTAMPTZ | NULL | | | approved_by | UUID | NULL REFERENCES public.ak_users(id) | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `dist_apply_user_policy` --- ### ak_distribution_agents - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | uid | UUID | PRIMARY KEY REFERENCES public.ak_users(id) ON DELETE CASCADE | | | division_uid | UUID | NOT NULL REFERENCES public.ak_distribution_divisions(uid), -- 所属事业部 | | | name | TEXT | NOT NULL | | | commission_ratio | NUMERIC(5,2) | DEFAULT 0 CHECK (commission_ratio >= 0 AND commission_ratio <= 100) | | | is_enabled | BOOLEAN | DEFAULT TRUE | | | end_time | TIMESTAMPTZ | | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | | created_by | UUID | REFERENCES public.ak_users(id) | | | updated_by | UUID | REFERENCES public.ak_users(id) | | **RLS 策略**: - `dist_agents_select_policy` --- ### ak_distribution_config - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | TEXT | PRIMARY KEY DEFAULT 'global_config' | | | is_enabled | BOOLEAN | DEFAULT true, -- 分销启用 | | | extract_type | TEXT | DEFAULT '2', -- 分销模式: 1指定, 2人人, 3满额 | | | bind_type | TEXT | DEFAULT '2', -- 绑定关系: 1所有用户, 2新用户 | | | store_brokerage_binding_status | TEXT | DEFAULT '1', -- 绑定模式: 1永久, 2有效期, 3临时 | | | brokerage_poster_status | TEXT | , -- 分销海报图路径/URL | | | brokerage_level | INTEGER | DEFAULT 2, -- 分销层级: 1, 2 | | | is_area_manager | BOOLEAN | DEFAULT true, -- 事业部开关 | | | is_agent_apply | BOOLEAN | DEFAULT true, -- 代理商申请开关 | | | is_commission_window | BOOLEAN | DEFAULT true, -- 佣金悬浮窗开关 | | | is_self_brokerage | BOOLEAN | DEFAULT true, -- 自购返佣 | | | is_member_brokerage | BOOLEAN | DEFAULT false, -- 购买会员返佣 | | | brokerage_type | TEXT | DEFAULT '1', -- 返佣类型: 1价格, 2实付 | | | is_promoter_brokerage | BOOLEAN | DEFAULT true, -- 推广用户返佣 | | | promoter_brokerage_price | DECIMAL(10,2) | DEFAULT 2.00 | | | promoter_brokerage_day_max | DECIMAL(10,2) | DEFAULT -1.00 | | | store_brokerage_ratio | DECIMAL(10,2) | DEFAULT 20.00 | | | store_brokerage_two_ratio | DECIMAL(10,2) | DEFAULT 2.00 | | | extract_frozen_time | INTEGER | DEFAULT 1 | | | user_extract_min_price | DECIMAL(10,2) | DEFAULT 1.00 | | | extract_bank_list | TEXT | DEFAULT '中国银行' | | | extract_type_list | TEXT[] | DEFAULT ARRAY['bank', 'wechat', 'alipay'] | | | wechat_extract_type | TEXT | DEFAULT '1' | | | alipay_extract_type | TEXT | DEFAULT '1' | | | user_extract_fee | DECIMAL(10,2) | DEFAULT 0.00 | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | | updated_by | UUID | REFERENCES auth.users(id) | | **RLS 策略**: - `dist_config_select_policy` --- ### ak_distribution_division_applications - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | division_id | UUID | NOT NULL REFERENCES public.ak_distribution_divisions(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL, -- 申请人填写的代理商名称 | | | phone | TEXT | NOT NULL, -- 申请人联系电话 | | | images | JSONB | DEFAULT '[]'::jsonb, -- 申请附件图片 (数组) | | | status | INTEGER | DEFAULT 1, -- 状态: 1待审核, 2已同意, 3已拒绝 | | | admin_remark | TEXT | , -- 审核备注 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | --- ### ak_distribution_divisions - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | uid | UUID | PRIMARY KEY REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL | | | invite_code | TEXT | UNIQUE NOT NULL | | | commission_ratio | NUMERIC(5,2) | DEFAULT 0 CHECK (commission_ratio >= 0 AND commission_ratio <= 100) | | | is_enabled | BOOLEAN | DEFAULT TRUE | | | end_time | TIMESTAMPTZ | | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | | created_by | UUID | REFERENCES public.ak_users(id) | | | updated_by | UUID | REFERENCES public.ak_users(id) | | **RLS 策略**: - `dist_divisions_select_policy` --- ### ak_distribution_level - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL, -- 等级名称 | | | level | INTEGER | NOT NULL UNIQUE, -- 等级权重/数字(如1, 2, 3) | | | percent1 | DECIMAL(10,2) | DEFAULT 0, -- 一级分佣比例 (%) | | | percent2 | DECIMAL(10,2) | DEFAULT 0, -- 二级分佣比例 (%) | | | task_total | INTEGER | DEFAULT 0, -- 任务总数 | | | task_finish | INTEGER | DEFAULT 0, -- 需完成数量(升级门槛) | | | is_visible | BOOLEAN | DEFAULT true, -- 是否显示 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `dist_level_select_policy` --- ### ak_promoter_relations - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | inviter_uid | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | bind_time | TIMESTAMPTZ | DEFAULT now() | | | created_at | TIMESTAMPTZ | DEFAULT now() | | **约束**: - `CONSTRAINT chk_ak_promoter_relations_no_self CHECK (uid <> inviter_uid)` - `CONSTRAINT uq_ak_promoter_relations_uid UNIQUE (uid)` **RLS 策略**: - `promoter_relations_select_policy` --- ## 财务 ### ml_extract - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) | | | real_name | TEXT | NULL, -- 提现人姓名快照 | | | extract_type | TEXT | NOT NULL, -- 提现方式: alipay, wechat, bank | | | alipay_code | TEXT | NULL, -- 支付宝账号 | | | wechat_code | TEXT | NULL, -- 微信账号 | | | bank_code | TEXT | NULL, -- 银行卡号 | | | bank_address | TEXT | NULL, -- 开户行地址 | | | extract_price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 申请提现金额 | | | service_fee | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 提现手续费 | | | balance | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 提现时的余额快照 | | | status | SMALLINT | NOT NULL DEFAULT 0, -- 状态: 0:待审核, 1:已通过, -1:已驳回 | | | refusal_reason | TEXT | NULL, -- 驳回原因 | | | admin_id | UUID | NULL REFERENCES public.ak_users(id), -- 审核人ID | | | payment_time | TIMESTAMPTZ | NULL, -- 打款/到账时间 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `ml_extract_user_select` **相关 RPC**(最多展示 8 个): - `rpc_admin_extract_list(p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_status SMALLINT DEFAULT NULL, p_start_ti...) -> JSONB` - `rpc_admin_extract_review(p_extract_id UUID, p_status SMALLINT, -- 1: 通过, -1: 驳回 p_refusal_reason TEXT DEFAULT NULL) -> VOID` --- ### ml_invoices - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) | | | order_no | TEXT | NOT NULL, -- 关联订单号 | | | order_amount | DECIMAL(12,2) | NOT NULL, -- 订单金额 | | | invoice_type | SMALLINT | NOT NULL DEFAULT 1, -- 1: 电子普通发票, 2: 增值税专用发票 | | | header_type | SMALLINT | NOT NULL DEFAULT 1, -- 1: 个人, 2: 企业 | | | header_name | TEXT | NOT NULL, -- 发票抬头 | | | tax_id | TEXT | NULL, -- 企业税号 | | | email | TEXT | NULL, -- 接收邮箱 | | | remark | TEXT | NULL, -- 备注 | | | status | SMALLINT | NOT NULL DEFAULT 0, -- 0: 待开票, 1: 已开票, -1: 已拒绝 | | | refusal_reason | TEXT | NULL, -- 驳回原因 | | | invoice_url | TEXT | NULL, -- 电子发票文件路径/URL | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `ml_invoices_user_select` --- ### ml_user_bill - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) | | | link_id | TEXT | NULL, -- 关联业务ID(订单号、提现ID、充值ID等) | | | pm | SMALLINT | NOT NULL DEFAULT 1, -- 0:支出, 1:收入 | | | title | TEXT | NOT NULL, -- 流水标题(如:商品购买、充值、提现) | | | category | TEXT | NOT NULL, -- 业务大类(如:balance-余额, integral-积分, brokerage-佣金) | | | type | TEXT | NOT NULL, -- 业务子类型(如:recharge, extract, pay, refund, system_add, system_sub) | | | number | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 变动金额 | | | balance | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 变动后的余额快照 | | | mark | TEXT | NULL, -- 备注 | | | status | SMALLINT | NOT NULL DEFAULT 1, -- 状态(1:有效, 0:无效/冲正) | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `ml_user_bill_user_select` **相关 RPC**(最多展示 8 个): - `rpc_admin_user_bill_list(p_page INTEGER DEFAULT 1, p_page_size INTEGER DEFAULT 15, p_category VARCHAR DEFAULT NULL, p_type VA...) -> JSONB` --- ### ml_user_recharge - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) | | | order_no | TEXT | UNIQUE NOT NULL, -- 充值订单号(cz开头) | | | recharge_type | TEXT | NOT NULL, -- 充值渠道: wechat, alipay, system (后台补单) | | | price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 实际充值金额 | | | give_price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 赠送金额 | | | paid | SMALLINT | NOT NULL DEFAULT 0, -- 支付状态: 0:未支付, 1:已支付 | | | pay_time | TIMESTAMPTZ | NULL, -- 支付时间 | | | channel_trade_no | TEXT | NULL, -- 外部渠道流水号 | | | status | SMALLINT | NOT NULL DEFAULT 1, -- 记录状态: 1:正常, 0:逻辑删除 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `ml_user_recharge_user_select` --- ## 内容与装修 ### ml_article_categories - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL | | | icon | TEXT | NULL | | | sort | INTEGER | NOT NULL DEFAULT 0 | | | status | SMALLINT | NOT NULL DEFAULT 1, -- 1: 启用, 0: 禁用 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `ml_article_categories_select_active` --- ### ml_articles - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | category_id | UUID | NOT NULL REFERENCES public.ml_article_categories(id) | | | title | TEXT | NOT NULL | | | author | TEXT | NULL | | | image | TEXT | NULL, -- 文章封面图 | | | description | TEXT | NULL, -- 文章简介 | | | content | TEXT | NOT NULL, -- 文章内容 (富文本) | | | status | SMALLINT | NOT NULL DEFAULT 0, -- 0: 未发布, 1: 已发布 | | | views | INTEGER | NOT NULL DEFAULT 0, -- 浏览量 | | | is_banner | BOOLEAN | NOT NULL DEFAULT FALSE, -- 是否展示在 banner | | | is_hot | BOOLEAN | NOT NULL DEFAULT FALSE, -- 是否热门 | | | linked_product_id | UUID | NULL, -- 关联商品ID (可选) | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `ml_articles_select_published` **相关 RPC**(最多展示 8 个): - `rpc_admin_article_save(p_id UUID DEFAULT NULL, p_category_id UUID DEFAULT NULL, p_title TEXT DEFAULT NULL, p_author TEXT DE...) -> UUID` - `rpc_admin_article_set_status(p_id UUID, p_status SMALLINT) -> BOOLEAN` --- ### ak_diy_pages - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL | | | type | TEXT | NOT NULL, -- home: 首页, topic: 专题页, user: 个人中心 | | | config | JSONB | NOT NULL DEFAULT '{}'::jsonb, -- 核心布局配置 (组件列表及参数) | | | is_home | BOOLEAN | NOT NULL DEFAULT FALSE, -- 是否为生效首页 | | | is_active | BOOLEAN | NOT NULL DEFAULT TRUE, -- 是否启用 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | created_by | UUID | REFERENCES public.ak_users(id) | | | updated_by | UUID | REFERENCES public.ak_users(id) | | **RLS 策略**: - `diy_pages_select_active` --- ## 客服 ### ml_kefu_accounts - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id), -- 关联主用户表 | | | nickname | TEXT | NOT NULL, -- 客服昵称 | | | avatar | TEXT | NULL, -- 客服头像 | | | status | SMALLINT | NOT NULL DEFAULT 1, -- 1:启用, 0:禁用 | | | is_online | BOOLEAN | NOT NULL DEFAULT FALSE | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS**:已启用,暂无显式策略(默认拒绝直接访问) **相关 RPC**(最多展示 8 个): - `rpc_admin_kefu_account_save(p_id UUID DEFAULT NULL, p_user_id UUID DEFAULT NULL, p_nickname TEXT DEFAULT NULL, p_avatar TEXT DEF...) -> UUID` - `rpc_admin_kefu_account_set_status(p_id UUID, p_status SMALLINT) -> BOOLEAN` --- ### ml_kefu_word_categories - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | name | TEXT | NOT NULL | | | sort | INT | NOT NULL DEFAULT 0 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS**:已启用,暂无显式策略(默认拒绝直接访问) --- ### ml_kefu_words - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | category_id | UUID | NOT NULL REFERENCES public.ml_kefu_word_categories(id) ON DELETE CASCADE | | | title | TEXT | NOT NULL | | | content | TEXT | NOT NULL | | | sort | INT | NOT NULL DEFAULT 0 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS**:已启用,暂无显式策略(默认拒绝直接访问) **相关 RPC**(最多展示 8 个): - `rpc_admin_kefu_word_save(p_id UUID DEFAULT NULL, p_category_id UUID DEFAULT NULL, p_title TEXT DEFAULT NULL, p_content TEXT D...) -> UUID` --- ### ml_kefu_feedbacks - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | user_id | UUID | NULL REFERENCES public.ak_users(id), -- 允许匿名留言 | | | nickname | TEXT | NULL | | | phone | TEXT | NULL | | | content | TEXT | NOT NULL | | | status | SMALLINT | NOT NULL DEFAULT 0, -- 0:未处理, 1:已处理 | | | reply_content | TEXT | NULL, -- 管理员回复内容 | | | processed_at | TIMESTAMPTZ | NULL, -- 处理时间 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS 策略**: - `ml_kefu_feedbacks_user_insert` - `ml_kefu_feedbacks_user_select` --- ### ml_kefu_auto_replies - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | keyword | TEXT | NOT NULL | | | content | TEXT | NOT NULL | | | reply_type | TEXT | NOT NULL DEFAULT 'text', -- text, image | | | status | SMALLINT | NOT NULL DEFAULT 1, -- 1:开启, 0:关闭 | | | created_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | | updated_at | TIMESTAMPTZ | NOT NULL DEFAULT now() | | **RLS**:已启用,暂无显式策略(默认拒绝直接访问) --- ## 系统配置 ### ml_system_configs - **所属端**:consumer / delivery | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | config_key | VARCHAR(100) | UNIQUE NOT NULL | | | config_value | JSONB | | | | description | TEXT | | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | updated_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | **RLS 策略**: - `system_configs_select_policy` **相关 RPC**(最多展示 8 个): - `rpc_admin_system_config_save(p_key TEXT, p_value JSONB, p_description TEXT DEFAULT NULL) -> BOOLEAN` --- ## 其他 ### ml_user_coupons - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | user_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | template_id | UUID | NOT NULL REFERENCES public.ml_coupon_templates(id) | | | coupon_code | VARCHAR(50) | UNIQUE NOT NULL | | | status | INTEGER | DEFAULT 1, -- 1:未使用2:已使用3:已过期 | | | used_at | TIMESTAMP | WITH TIME ZONE | | | order_id | UUID | REFERENCES public.ml_orders(id) | | | received_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | | expire_at | TIMESTAMP | WITH TIME ZONE NOT NULL | | **约束**: - `CONSTRAINT chk_ml_user_coupon_status CHECK (status IN (1,2,3))` --- ### ml_regions - **所属端**:consumer | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT uuid_generate_v4() | | | parent_id | UUID | REFERENCES public.ml_regions(id) | | | name | VARCHAR(100) | NOT NULL | | | code | VARCHAR(20) | | | | level | INTEGER | NOT NULL, -- 1:省份 2:城市 3:区县 4:街道 | | | sort_order | INTEGER | DEFAULT 0 | | | is_active | BOOLEAN | DEFAULT TRUE | | | created_at | TIMESTAMP | WITH TIME ZONE DEFAULT NOW() | | --- ### ak_seckill_activities - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | title | TEXT | NOT NULL, -- 活动标题 | | | single_limit | INTEGER | DEFAULT 1, -- 单次限购 | | | total_limit | INTEGER | DEFAULT 10, -- 总购买数量限制 | | | product_count | INTEGER | DEFAULT 0, -- 包含商品数量 | | | time_range | TEXT | NOT NULL, -- 活动时段 (如 "06:00-24:00") | | | start_date | TIMESTAMPTZ | NOT NULL, -- 开始日期 | | | end_date | TIMESTAMPTZ | NOT NULL, -- 结束日期 | | | status | BOOLEAN | DEFAULT true, -- 状态: true开启, false关闭 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `seckill_select_policy` --- ### ak_combination_activities - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id), -- 开团团长 | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id), -- 拼团商品 | | | people | INTEGER | DEFAULT 2, -- 几人团 | | | count_people | INTEGER | DEFAULT 1, -- 当前几人参加 | | | start_time | TIMESTAMPTZ | DEFAULT now(), -- 开团时间 | | | stop_time | TIMESTAMPTZ | NOT NULL, -- 结束时间 | | | status | TEXT | NOT NULL DEFAULT 'ongoing', -- ongoing进行中, pending未完成, ended已成功 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **约束**: - `CONSTRAINT chk_comb_status CHECK (status IN ('ongoing', 'pending', 'ended'))` **RLS 策略**: - `combination_select_policy` --- ### ak_marketing_bargains - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | title | TEXT | NOT NULL, -- 活动标题 | | | min_price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 砍价最低价 | | | stock | INTEGER | DEFAULT 0, -- 活动库存 | | | start_time | TIMESTAMPTZ | NOT NULL, -- 开始时间 | | | stop_time | TIMESTAMPTZ | NOT NULL, -- 结束时间 | | | status | BOOLEAN | DEFAULT true, -- 状态: true开启, false关闭 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `bargain_select_policy` --- ### ak_marketing_groupbuys - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | title | TEXT | NOT NULL, -- 活动标题 | | | price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 团购价格 | | | people | INTEGER | DEFAULT 2, -- 成团人数要求 | | | stock | INTEGER | DEFAULT 0, -- 活动库存 | | | start_time | TIMESTAMPTZ | NOT NULL, -- 开始时间 | | | stop_time | TIMESTAMPTZ | NOT NULL, -- 结束时间 | | | status | BOOLEAN | DEFAULT true, -- 状态: true开启, false关闭 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `groupbuy_select_policy` --- ### ak_marketing_live_products - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | live_price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 直播专属价 | | | stock | INTEGER | DEFAULT 0, -- 直播可用库存 | | | audit_status | INTEGER | DEFAULT 1, -- 审核状态: 1待审核, 2审核通过, 3审核驳回 | | | is_show | BOOLEAN | DEFAULT true, -- 是否在直播间显示 | | | sort_order | INTEGER | DEFAULT 0 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | --- ### ak_marketing_lotteries - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL, -- 活动名称 | | | type | INTEGER | DEFAULT 1, -- 活动类型: 1积分抽奖, 2订单评价, 3订单支付 | | | start_time | TIMESTAMPTZ | NOT NULL, -- 开始时间 | | | end_time | TIMESTAMPTZ | NOT NULL, -- 结束时间 | | | is_open | BOOLEAN | DEFAULT true, -- 是否开启 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `lottery_select_policy` --- ### ak_marketing_lottery_prizes - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | lottery_id | UUID | NOT NULL REFERENCES public.ak_marketing_lotteries(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL, -- 奖品名称 | | | prize_type | TEXT | NOT NULL, -- 奖品类型: points, balance, coupon, physical | | | amount | DECIMAL(12,2) | DEFAULT 0, -- 奖励面值/数量 | | | stock | INTEGER | DEFAULT 0, -- 奖品库存 | | | probability | DECIMAL(5,2) | DEFAULT 0, -- 中奖概率 (0-100) | | | sort_order | INTEGER | DEFAULT 0 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | --- ### ak_marketing_live_anchors - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | nickname | TEXT | NOT NULL, -- 主播昵称 | | | wechat | TEXT | , -- 微信号 | | | phone | TEXT | , -- 联系电话 | | | avatar_url | TEXT | , -- 头像 | | | status | BOOLEAN | DEFAULT true, -- 状态 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `live_anchors_select_policy` --- ### ak_marketing_live_rooms - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | anchor_id | UUID | REFERENCES public.ak_marketing_live_anchors(id) ON DELETE SET NULL | | | name | TEXT | NOT NULL, -- 直播间名称 | | | background_url | TEXT | , -- 背景图 | | | share_img_url | TEXT | , -- 分享图 | | | start_time | TIMESTAMPTZ | NOT NULL, -- 开始时间 | | | end_time | TIMESTAMPTZ | NOT NULL, -- 计划结束时间 | | | sort | INTEGER | DEFAULT 0, -- 排序 | | | type | TEXT | DEFAULT 'phone', -- 类型: phone手机直播等 | | | like_enabled | BOOLEAN | DEFAULT true, -- 开启点赞 | | | sale_enabled | BOOLEAN | DEFAULT true, -- 开启卖货 | | | comment_enabled | BOOLEAN | DEFAULT true, -- 开启评论 | | | is_show | BOOLEAN | DEFAULT true, -- 是否显示 | | | live_status | INTEGER | DEFAULT 1, -- 1未开始, 2直播中, 3暂停, 4已结束 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `live_rooms_select_policy` --- ### ak_marketing_checkin_configs - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | TEXT | PRIMARY KEY DEFAULT 'checkin_config' | | | merchant_id | UUID | NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE | | | is_open | BOOLEAN | DEFAULT true, -- 签到开关 | | | mode | TEXT | DEFAULT 'none', -- 签到模式: none(无限制), week(周循环), month(月循环) | | | notice_enabled | BOOLEAN | DEFAULT false, -- 签到提醒开关 | | | integral_reward | INTEGER | DEFAULT 10, -- 每日签到赠送积分 | | | exp_reward | INTEGER | DEFAULT 1, -- 每日签到赠送经验 | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | | updated_by | UUID | REFERENCES auth.users(id) | | --- ### ak_marketing_newcomer_config - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | TEXT | PRIMARY KEY DEFAULT 'newcomer_config' | | | merchant_id | UUID | NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE | | | balance_reward | DECIMAL(12,2) | DEFAULT 0.00, -- 赠送余额 | | | integral_reward | INTEGER | DEFAULT 0, -- 赠送积分 | | | coupons_json | JSONB | DEFAULT '[]'::jsonb | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | | updated_by | UUID | REFERENCES auth.users(id) | | **RLS 策略**: - `newcomer_config_select_policy` --- ### ak_marketing_signin_logs - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | uid | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | points | INTEGER | NOT NULL DEFAULT 0, -- 本次签到获得的积分 | | | is_continuous_reward | BOOLEAN | DEFAULT false, -- 是否包含连续签到额外奖励 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `signin_logs_user_policy` --- ### ak_marketing_member_types - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL, -- 会员名 (如: 月卡, 年卡) | | | duration_days | INTEGER | DEFAULT 30, -- 有效期(天),0表示永久 | | | price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 原价 | | | discount_price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 优惠价/实际支付价 | | | is_open | BOOLEAN | DEFAULT true, -- 是否开启 | | | sort_order | INTEGER | DEFAULT 0, -- 排序 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `member_types_select_policy` --- ### ak_marketing_member_rights - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL, -- 权益名称 | | | description | TEXT | , -- 权益简介 | | | icon_url | TEXT | , -- 权益图标 | | | is_show | BOOLEAN | DEFAULT true, -- 是否展示 | | | sort_order | INTEGER | DEFAULT 0, -- 排序 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `member_rights_select_policy` --- ### ak_marketing_member_config - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | TEXT | PRIMARY KEY DEFAULT 'member_config' | | | merchant_id | UUID | NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE | | | is_enabled | BOOLEAN | DEFAULT true, -- 是否开启付费会员功能 | | | bg_img_url | TEXT | , -- 会员期内背景图 | | | expire_bg_img_url | TEXT | , -- 会员到期背景图 | | | rules_description | TEXT | , -- 会员规则说明文本 | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `member_config_select_policy` --- ### ak_recharge_configs - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | TEXT | PRIMARY KEY DEFAULT 'recharge_config' | | | merchant_id | UUID | NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE | | | balance_enabled | BOOLEAN | DEFAULT true, -- 余额功能是否启用 | | | recharge_notice | TEXT | , -- 充值注意事项说明 | | | mp_recharge_enabled | BOOLEAN | DEFAULT false, -- 小程序充值开关 | | | min_recharge_amount | DECIMAL(12,2) | DEFAULT 0.01, -- 最低充值金额 | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `recharge_config_select_policy` --- ### ak_recharge_quotas - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 售价(实际充值金额) | | | bonus_price | DECIMAL(12,2) | NOT NULL DEFAULT 0, -- 赠送金额 | | | is_open | BOOLEAN | DEFAULT true, -- 是否可用 | | | sort_order | INTEGER | DEFAULT 0, -- 排序 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **RLS 策略**: - `recharge_quotas_select_policy` --- ### ak_signin_configs - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | TEXT | PRIMARY KEY DEFAULT 'signin_config', -- 每个商家一个配置记录 | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | is_enabled | BOOLEAN | DEFAULT true, -- 签到功能是否启用 | | | daily_points | INTEGER | DEFAULT 10, -- 每日签到固定奖励积分 | | | continuous_rewards | JSONB | DEFAULT '[]'::jsonb | | | rules_description | TEXT | DEFAULT '1.每日签到可获得积分奖励;\n2.连续签到满足天数可获得额外阶梯奖励;\n3.签到中断将重新从第一天开始计算。' | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | | updated_by | UUID | REFERENCES auth.users(id) | | **约束**: - `UNIQUE(merchant_id)` **RLS 策略**: - `signin_config_select_policy` --- ### ak_product_label_groups - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL | | | sort_order | INTEGER | DEFAULT 0 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | --- ### ak_product_labels - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | group_id | UUID | REFERENCES public.ak_product_label_groups(id) ON DELETE SET NULL | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL | | | is_active | BOOLEAN | DEFAULT true | | | show_in_mobile | BOOLEAN | DEFAULT true | | | sort_order | INTEGER | DEFAULT 0 | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | --- ### ak_product_member_prices - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | product_id | UUID | NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE | | | sku_id | UUID | NOT NULL REFERENCES public.ml_product_skus(id) ON DELETE CASCADE | | | level_id | UUID | NOT NULL REFERENCES public.ak_user_levels(id) ON DELETE CASCADE | | | member_price | DECIMAL(12,2) | NOT NULL CHECK (member_price >= 0) | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | **约束**: - `UNIQUE(merchant_id, sku_id, level_id)` --- ### ak_product_protections - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL | | | description | TEXT | NOT NULL DEFAULT '' | | | icon_url | TEXT | | | | sort_order | INTEGER | DEFAULT 0 | | | is_active | BOOLEAN | DEFAULT true | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | --- ### ak_product_spec_templates - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL | | | specs | TEXT | NOT NULL DEFAULT '' | | | attrs | TEXT | NOT NULL DEFAULT '' | | | sort_order | INTEGER | DEFAULT 0 | | | is_active | BOOLEAN | DEFAULT true | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | --- ### ak_product_param_templates - **所属端**:admin / 系统 | 字段名 | 数据类型 | 约束/默认值 | 说明 | |--------|----------|-------------|------| | id | UUID | PRIMARY KEY DEFAULT gen_random_uuid() | | | merchant_id | UUID | NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE | | | name | TEXT | NOT NULL | | | sort_order | INTEGER | DEFAULT 0 | | | params | JSONB | NOT NULL DEFAULT '[]'::jsonb | | | is_active | BOOLEAN | DEFAULT true | | | created_at | TIMESTAMPTZ | DEFAULT now() | | | updated_at | TIMESTAMPTZ | DEFAULT now() | | --- ## 完整对接 SQL 汇总 > 以下 SQL 按执行顺序排列:Schema → RLS → RPC。请在新项目/新环境中按顺序执行。 ### 1) 核心商城 Schema(complete_mall_database.sql) ```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/) ```sql -- ===================================================================================== -- 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(行级安全) ```sql -- ===================================================================================== -- 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 目录) ```sql -- ===================================================================================== -- 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 | |