admin模块接入数据库
This commit is contained in:
43
docs/ops/2026-02-11__admin__cms-module-repaired-full.md
Normal file
43
docs/ops/2026-02-11__admin__cms-module-repaired-full.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 内容管理模块 (CMS) 全量修复与数据库构建报告
|
||||
|
||||
## 摘要
|
||||
本次对 Admin 侧内容管理模块(CMS)进行了深度的端到端修复,补齐了完全缺失的数据库表结构、行级安全策略(RLS)以及管理端 RPC 接口。同时重构了文章管理和分类管理两个核心页面,彻底解决了该模块此前的全量 Mock 问题。
|
||||
|
||||
## 修复范围
|
||||
|
||||
### 1. 数据库构建 (Schema & RLS)
|
||||
- **核心表创建**:新增了 `ml_article_categories`(文章分类)和 `ml_articles`(文章主表)。
|
||||
- **安全隔离**:配置了 RLS 策略,确保消费者端仅能查看“已发布”的内容,管理端通过 RPC 拥有全量管理权限。
|
||||
|
||||
### 2. RPC 接口补全 (SECURITY DEFINER)
|
||||
- 实现了 9 个标准 RPC 接口,涵盖:
|
||||
- 文章分类的分页查询、保存、删除及状态切换。
|
||||
- 文章内容的分页查询、详情获取、内容保存、逻辑删除及发布状态管理。
|
||||
|
||||
### 3. 前端重构 (去 Mock)
|
||||
- **服务层封装**:新建了 `services/admin/cmsService.uts`,统一封装了所有 CMS 相关的真实后端调用。
|
||||
- **分类管理 (`category/list.uvue`)**:实现了分类的分页列表展示、新增/编辑抽屉、状态切换及物理删除交互。
|
||||
- **文章管理 (`article/list.uvue`)**:实现了文章的分页列表、分类筛选、关键词搜索、发布状态切换及内容编辑保存。
|
||||
|
||||
## 变更清单
|
||||
|
||||
### 数据库 SQL
|
||||
- `docs/sql/10_schema/cms/ml_cms_tables_v1.sql` (新增)
|
||||
- `docs/sql/20_rls/cms/ml_cms_rls_v1.sql` (新增)
|
||||
- `docs/sql/30_rpc/cms/` 目录下共 9 个接口 SQL 文件。
|
||||
|
||||
### 前端代码
|
||||
- `services/admin/cmsService.uts` (新增)
|
||||
- `pages/mall/admin/cms/article/list.uvue` (逻辑与 UI 重构)
|
||||
- `pages/mall/admin/cms/category/list.uvue` (逻辑与 UI 重构)
|
||||
|
||||
## 验证说明
|
||||
1. **数据库执行**:需依次执行 `10_schema` -> `20_rls` -> `30_rpc` 下的 CMS 模块脚本。
|
||||
2. **功能验证**:
|
||||
- 进入文章分类:确认添加分类后,在文章管理中分类下拉列表能正确显示。
|
||||
- 文章发布:确认在文章列表切换发布开关后,状态能真实同步到数据库。
|
||||
- 筛选功能:确认分类筛选和标题搜索能过滤出正确的结果。
|
||||
|
||||
## 关联规范
|
||||
- 遵循 `AGENT_PROJECT_SPEC.md` 规范。
|
||||
- 遵循统一的 RPC 入口鉴权(admin/analytics 角色)。
|
||||
49
docs/sql/10_schema/cms/ml_cms_tables_v1.sql
Normal file
49
docs/sql/10_schema/cms/ml_cms_tables_v1.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- =====================================================================================
|
||||
-- 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);
|
||||
61
docs/sql/10_schema/distribution/ak_commission_logs_v1.sql
Normal file
61
docs/sql/10_schema/distribution/ak_commission_logs_v1.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- 佣金流水表:记录每一笔佣金变动(冻结/可用/已提现/取消)
|
||||
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());
|
||||
@@ -0,0 +1,72 @@
|
||||
-- 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;
|
||||
45
docs/sql/10_schema/distribution/ak_distribution_level_v1.sql
Normal file
45
docs/sql/10_schema/distribution/ak_distribution_level_v1.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- 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;
|
||||
41
docs/sql/10_schema/distribution/ak_promoter_relations_v1.sql
Normal file
41
docs/sql/10_schema/distribution/ak_promoter_relations_v1.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- 推广员关系表:记录下级与上级(邀请人)的绑定关系
|
||||
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());
|
||||
46
docs/sql/10_schema/product/ak_product_labels_v1.sql
Normal file
46
docs/sql/10_schema/product/ak_product_labels_v1.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- 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);
|
||||
47
docs/sql/10_schema/product/ak_product_member_prices_v1.sql
Normal file
47
docs/sql/10_schema/product/ak_product_member_prices_v1.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
-- =====================================================================================
|
||||
-- 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);
|
||||
24
docs/sql/10_schema/product/ak_product_protections_v1.sql
Normal file
24
docs/sql/10_schema/product/ak_product_protections_v1.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- 商品保障/服务条款(按 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());
|
||||
49
docs/sql/10_schema/product/ak_product_templates_v1.sql
Normal file
49
docs/sql/10_schema/product/ak_product_templates_v1.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- 商品规格模板表 + 商品参数模板表(按 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());
|
||||
19
docs/sql/10_schema/user/ak_users_add_phone_real_name_v1.sql
Normal file
19
docs/sql/10_schema/user/ak_users_add_phone_real_name_v1.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 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 $$;
|
||||
29
docs/sql/20_rls/cms/ml_cms_rls_v1.sql
Normal file
29
docs/sql/20_rls/cms/ml_cms_rls_v1.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- =====================================================================================
|
||||
-- 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);
|
||||
|
||||
-- 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);
|
||||
|
||||
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,管理端操作通过 RPC (SECURITY DEFINER) 执行
|
||||
34
docs/sql/20_rls/marketing/ml_coupon_templates_rls_v1.sql
Normal file
34
docs/sql/20_rls/marketing/ml_coupon_templates_rls_v1.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- =====================================================================================
|
||||
-- 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);
|
||||
47
docs/sql/30_rpc/cms/rpc_admin_article_category_delete_v1.sql
Normal file
47
docs/sql/30_rpc/cms/rpc_admin_article_category_delete_v1.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
-- =====================================================================================
|
||||
-- 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_has_articles BOOLEAN;
|
||||
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. 检查是否有关联文章
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.ml_articles
|
||||
WHERE category_id = p_id
|
||||
) INTO v_has_articles;
|
||||
|
||||
IF v_has_articles THEN
|
||||
RAISE EXCEPTION 'Cannot delete category with associated articles';
|
||||
END IF;
|
||||
|
||||
-- 3. 执行物理删除
|
||||
DELETE FROM public.ml_article_categories WHERE id = p_id;
|
||||
|
||||
GET DIAGNOSTICS v_ok = ROW_COUNT;
|
||||
RETURN v_ok;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_article_category_delete IS '管理员删除文章分类(含关联性检查)';
|
||||
55
docs/sql/30_rpc/cms/rpc_admin_article_category_list_v1.sql
Normal file
55
docs/sql/30_rpc/cms/rpc_admin_article_category_list_v1.sql
Normal file
@@ -0,0 +1,55 @@
|
||||
-- =====================================================================================
|
||||
-- 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;
|
||||
$$;
|
||||
65
docs/sql/30_rpc/cms/rpc_admin_article_category_save_v1.sql
Normal file
65
docs/sql/30_rpc/cms/rpc_admin_article_category_save_v1.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- =====================================================================================
|
||||
-- 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 '管理员新增或更新文章分类';
|
||||
@@ -0,0 +1,40 @@
|
||||
-- =====================================================================================
|
||||
-- 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 '管理员设置文章分类状态';
|
||||
36
docs/sql/30_rpc/cms/rpc_admin_article_delete_v1.sql
Normal file
36
docs/sql/30_rpc/cms/rpc_admin_article_delete_v1.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- =====================================================================================
|
||||
-- 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;
|
||||
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. 执行物理删除
|
||||
DELETE FROM public.ml_articles WHERE id = p_id;
|
||||
|
||||
GET DIAGNOSTICS v_ok = ROW_COUNT;
|
||||
RETURN v_ok;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_article_delete IS '管理员删除文章记录';
|
||||
58
docs/sql/30_rpc/cms/rpc_admin_article_get_detail_v1.sql
Normal file
58
docs/sql/30_rpc/cms/rpc_admin_article_get_detail_v1.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- =====================================================================================
|
||||
-- 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 '管理员获取文章完整详情';
|
||||
77
docs/sql/30_rpc/cms/rpc_admin_article_list_v1.sql
Normal file
77
docs/sql/30_rpc/cms/rpc_admin_article_list_v1.sql
Normal file
@@ -0,0 +1,77 @@
|
||||
-- =====================================================================================
|
||||
-- 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 '管理员分页查询文章列表';
|
||||
82
docs/sql/30_rpc/cms/rpc_admin_article_save_v1.sql
Normal file
82
docs/sql/30_rpc/cms/rpc_admin_article_save_v1.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- =====================================================================================
|
||||
-- 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 '管理员新增或更新文章内容';
|
||||
40
docs/sql/30_rpc/cms/rpc_admin_article_set_status_v1.sql
Normal file
40
docs/sql/30_rpc/cms/rpc_admin_article_set_status_v1.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- =====================================================================================
|
||||
-- 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 '管理员设置文章发布状态';
|
||||
133
docs/sql/30_rpc/distribution/rpc_admin_get_promoter_list_v1.sql
Normal file
133
docs/sql/30_rpc/distribution/rpc_admin_get_promoter_list_v1.sql
Normal file
@@ -0,0 +1,133 @@
|
||||
-- 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;
|
||||
81
docs/sql/30_rpc/product/rpc_admin_get_product_reviews_v1.sql
Normal file
81
docs/sql/30_rpc/product/rpc_admin_get_product_reviews_v1.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user