From ec636dc703a396b7dee301071723bba50407b872 Mon Sep 17 00:00:00 2001 From: comlibmb <1844410276@qq.com> Date: Fri, 13 Feb 2026 17:29:50 +0800 Subject: [PATCH] =?UTF-8?q?admin=E6=A8=A1=E5=9D=97=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-02-11__admin__cms-module-repaired-full.md | 43 ++ docs/sql/10_schema/cms/ml_cms_tables_v1.sql | 49 ++ .../distribution/ak_commission_logs_v1.sql | 61 ++ .../ak_distribution_config_v1.sql | 72 ++ .../distribution/ak_distribution_level_v1.sql | 45 ++ .../distribution/ak_promoter_relations_v1.sql | 41 ++ .../product/ak_product_labels_v1.sql | 46 ++ .../product/ak_product_member_prices_v1.sql | 47 ++ .../product/ak_product_protections_v1.sql | 24 + .../product/ak_product_templates_v1.sql | 49 ++ .../user/ak_users_add_phone_real_name_v1.sql | 19 + docs/sql/20_rls/cms/ml_cms_rls_v1.sql | 29 + .../marketing/ml_coupon_templates_rls_v1.sql | 34 + .../rpc_admin_article_category_delete_v1.sql | 47 ++ .../rpc_admin_article_category_list_v1.sql | 55 ++ .../rpc_admin_article_category_save_v1.sql | 65 ++ ...c_admin_article_category_set_status_v1.sql | 40 ++ .../cms/rpc_admin_article_delete_v1.sql | 36 + .../cms/rpc_admin_article_get_detail_v1.sql | 58 ++ .../30_rpc/cms/rpc_admin_article_list_v1.sql | 77 ++ .../30_rpc/cms/rpc_admin_article_save_v1.sql | 82 +++ .../cms/rpc_admin_article_set_status_v1.sql | 40 ++ .../rpc_admin_get_promoter_list_v1.sql | 133 ++++ .../rpc_admin_get_product_reviews_v1.sql | 81 +++ layouts/admin/router/adminComponentMap.uts | 2 +- pages/mall/admin/cms/article/list.uvue | 531 ++++++++------ pages/mall/admin/cms/category/list.uvue | 301 +++++--- .../mall/admin/distribution/level/index.uvue | 273 ++++++- .../admin/distribution/promoter/index.uvue | 131 +++- .../admin/distribution/setting/index.uvue | 92 ++- .../mall/admin/finance/transaction_stats.uvue | 4 + pages/mall/admin/marketing/coupon/list.uvue | 220 ++++-- .../admin/order/aftersales-order/index.uvue | 33 +- .../mall/admin/order/cashier-order/index.uvue | 38 +- pages/mall/admin/order/list.uvue | 538 +------------- .../admin/order/order-management/index.uvue | 413 ++++++++++- .../admin/order/order-statistics/index.uvue | 49 +- .../admin/order/write-off-records/index.uvue | 35 +- pages/mall/admin/product/labels/index.uvue | 198 +++-- pages/mall/admin/product/list.uvue | 17 +- .../mall/admin/product/parameters/index.uvue | 181 +++-- .../product/product-management/edit.uvue | 676 +++++++++++++++--- .../product-management/member-price.uvue | 201 +++++- .../product/product-statistics/index.uvue | 41 +- .../mall/admin/product/protection/index.uvue | 121 ++-- pages/mall/admin/product/reviews/index.uvue | 274 +++++-- .../admin/product/specifications/index.uvue | 117 ++- services/admin/cmsService.uts | 180 +++++ services/admin/distributionService.uts | 199 ++++++ services/admin/marketingService.uts | 139 ++++ services/admin/productLabelService.uts | 147 ++++ services/admin/productMemberPriceService.uts | 119 +++ services/admin/productProtectionService.uts | 85 +++ services/admin/productReviewService.uts | 85 +++ services/admin/productService.uts | 102 +++ services/admin/productSpecParamService.uts | 125 ++++ services/orderService.uts | 16 +- utils/authRedirect.uts | 24 +- 58 files changed, 5586 insertions(+), 1394 deletions(-) create mode 100644 docs/ops/2026-02-11__admin__cms-module-repaired-full.md create mode 100644 docs/sql/10_schema/cms/ml_cms_tables_v1.sql create mode 100644 docs/sql/10_schema/distribution/ak_commission_logs_v1.sql create mode 100644 docs/sql/10_schema/distribution/ak_distribution_config_v1.sql create mode 100644 docs/sql/10_schema/distribution/ak_distribution_level_v1.sql create mode 100644 docs/sql/10_schema/distribution/ak_promoter_relations_v1.sql create mode 100644 docs/sql/10_schema/product/ak_product_labels_v1.sql create mode 100644 docs/sql/10_schema/product/ak_product_member_prices_v1.sql create mode 100644 docs/sql/10_schema/product/ak_product_protections_v1.sql create mode 100644 docs/sql/10_schema/product/ak_product_templates_v1.sql create mode 100644 docs/sql/10_schema/user/ak_users_add_phone_real_name_v1.sql create mode 100644 docs/sql/20_rls/cms/ml_cms_rls_v1.sql create mode 100644 docs/sql/20_rls/marketing/ml_coupon_templates_rls_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_category_delete_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_category_list_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_category_save_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_category_set_status_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_delete_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_get_detail_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_list_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_save_v1.sql create mode 100644 docs/sql/30_rpc/cms/rpc_admin_article_set_status_v1.sql create mode 100644 docs/sql/30_rpc/distribution/rpc_admin_get_promoter_list_v1.sql create mode 100644 docs/sql/30_rpc/product/rpc_admin_get_product_reviews_v1.sql create mode 100644 services/admin/cmsService.uts create mode 100644 services/admin/distributionService.uts create mode 100644 services/admin/marketingService.uts create mode 100644 services/admin/productLabelService.uts create mode 100644 services/admin/productMemberPriceService.uts create mode 100644 services/admin/productProtectionService.uts create mode 100644 services/admin/productReviewService.uts create mode 100644 services/admin/productSpecParamService.uts diff --git a/docs/ops/2026-02-11__admin__cms-module-repaired-full.md b/docs/ops/2026-02-11__admin__cms-module-repaired-full.md new file mode 100644 index 00000000..d2e4c6de --- /dev/null +++ b/docs/ops/2026-02-11__admin__cms-module-repaired-full.md @@ -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 角色)。 diff --git a/docs/sql/10_schema/cms/ml_cms_tables_v1.sql b/docs/sql/10_schema/cms/ml_cms_tables_v1.sql new file mode 100644 index 00000000..0bee3dc4 --- /dev/null +++ b/docs/sql/10_schema/cms/ml_cms_tables_v1.sql @@ -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); diff --git a/docs/sql/10_schema/distribution/ak_commission_logs_v1.sql b/docs/sql/10_schema/distribution/ak_commission_logs_v1.sql new file mode 100644 index 00000000..4c02a895 --- /dev/null +++ b/docs/sql/10_schema/distribution/ak_commission_logs_v1.sql @@ -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()); diff --git a/docs/sql/10_schema/distribution/ak_distribution_config_v1.sql b/docs/sql/10_schema/distribution/ak_distribution_config_v1.sql new file mode 100644 index 00000000..4181d251 --- /dev/null +++ b/docs/sql/10_schema/distribution/ak_distribution_config_v1.sql @@ -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; diff --git a/docs/sql/10_schema/distribution/ak_distribution_level_v1.sql b/docs/sql/10_schema/distribution/ak_distribution_level_v1.sql new file mode 100644 index 00000000..96d82bea --- /dev/null +++ b/docs/sql/10_schema/distribution/ak_distribution_level_v1.sql @@ -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; diff --git a/docs/sql/10_schema/distribution/ak_promoter_relations_v1.sql b/docs/sql/10_schema/distribution/ak_promoter_relations_v1.sql new file mode 100644 index 00000000..2d58a313 --- /dev/null +++ b/docs/sql/10_schema/distribution/ak_promoter_relations_v1.sql @@ -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()); diff --git a/docs/sql/10_schema/product/ak_product_labels_v1.sql b/docs/sql/10_schema/product/ak_product_labels_v1.sql new file mode 100644 index 00000000..83958783 --- /dev/null +++ b/docs/sql/10_schema/product/ak_product_labels_v1.sql @@ -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); diff --git a/docs/sql/10_schema/product/ak_product_member_prices_v1.sql b/docs/sql/10_schema/product/ak_product_member_prices_v1.sql new file mode 100644 index 00000000..2aa6875d --- /dev/null +++ b/docs/sql/10_schema/product/ak_product_member_prices_v1.sql @@ -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); diff --git a/docs/sql/10_schema/product/ak_product_protections_v1.sql b/docs/sql/10_schema/product/ak_product_protections_v1.sql new file mode 100644 index 00000000..e4778029 --- /dev/null +++ b/docs/sql/10_schema/product/ak_product_protections_v1.sql @@ -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()); diff --git a/docs/sql/10_schema/product/ak_product_templates_v1.sql b/docs/sql/10_schema/product/ak_product_templates_v1.sql new file mode 100644 index 00000000..88155ac1 --- /dev/null +++ b/docs/sql/10_schema/product/ak_product_templates_v1.sql @@ -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()); diff --git a/docs/sql/10_schema/user/ak_users_add_phone_real_name_v1.sql b/docs/sql/10_schema/user/ak_users_add_phone_real_name_v1.sql new file mode 100644 index 00000000..e483a0da --- /dev/null +++ b/docs/sql/10_schema/user/ak_users_add_phone_real_name_v1.sql @@ -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 $$; diff --git a/docs/sql/20_rls/cms/ml_cms_rls_v1.sql b/docs/sql/20_rls/cms/ml_cms_rls_v1.sql new file mode 100644 index 00000000..e7b18723 --- /dev/null +++ b/docs/sql/20_rls/cms/ml_cms_rls_v1.sql @@ -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) 执行 diff --git a/docs/sql/20_rls/marketing/ml_coupon_templates_rls_v1.sql b/docs/sql/20_rls/marketing/ml_coupon_templates_rls_v1.sql new file mode 100644 index 00000000..911f8822 --- /dev/null +++ b/docs/sql/20_rls/marketing/ml_coupon_templates_rls_v1.sql @@ -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); diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_category_delete_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_category_delete_v1.sql new file mode 100644 index 00000000..4e4bdf6e --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_category_delete_v1.sql @@ -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 '管理员删除文章分类(含关联性检查)'; diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_category_list_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_category_list_v1.sql new file mode 100644 index 00000000..910fe16e --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_category_list_v1.sql @@ -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; +$$; diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_category_save_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_category_save_v1.sql new file mode 100644 index 00000000..8b658853 --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_category_save_v1.sql @@ -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 '管理员新增或更新文章分类'; diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_category_set_status_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_category_set_status_v1.sql new file mode 100644 index 00000000..8dbc0187 --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_category_set_status_v1.sql @@ -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 '管理员设置文章分类状态'; diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_delete_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_delete_v1.sql new file mode 100644 index 00000000..c14e693e --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_delete_v1.sql @@ -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 '管理员删除文章记录'; diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_get_detail_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_get_detail_v1.sql new file mode 100644 index 00000000..31a85af9 --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_get_detail_v1.sql @@ -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 '管理员获取文章完整详情'; diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_list_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_list_v1.sql new file mode 100644 index 00000000..91c105d6 --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_list_v1.sql @@ -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 '管理员分页查询文章列表'; diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_save_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_save_v1.sql new file mode 100644 index 00000000..29823a33 --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_save_v1.sql @@ -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 '管理员新增或更新文章内容'; diff --git a/docs/sql/30_rpc/cms/rpc_admin_article_set_status_v1.sql b/docs/sql/30_rpc/cms/rpc_admin_article_set_status_v1.sql new file mode 100644 index 00000000..71d7a922 --- /dev/null +++ b/docs/sql/30_rpc/cms/rpc_admin_article_set_status_v1.sql @@ -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 '管理员设置文章发布状态'; diff --git a/docs/sql/30_rpc/distribution/rpc_admin_get_promoter_list_v1.sql b/docs/sql/30_rpc/distribution/rpc_admin_get_promoter_list_v1.sql new file mode 100644 index 00000000..6c5d311b --- /dev/null +++ b/docs/sql/30_rpc/distribution/rpc_admin_get_promoter_list_v1.sql @@ -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; diff --git a/docs/sql/30_rpc/product/rpc_admin_get_product_reviews_v1.sql b/docs/sql/30_rpc/product/rpc_admin_get_product_reviews_v1.sql new file mode 100644 index 00000000..0ac820c9 --- /dev/null +++ b/docs/sql/30_rpc/product/rpc_admin_get_product_reviews_v1.sql @@ -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; diff --git a/layouts/admin/router/adminComponentMap.uts b/layouts/admin/router/adminComponentMap.uts index 9e3a80db..81196b37 100644 --- a/layouts/admin/router/adminComponentMap.uts +++ b/layouts/admin/router/adminComponentMap.uts @@ -54,7 +54,7 @@ export const componentMap: Map = new Map([ ['ProductProtection', defineAsyncComponent(() => import('@/pages/mall/admin/product/protection/index.uvue'))], // 订单模块 - ['OrderList', defineAsyncComponent(() => import('@/pages/mall/admin/order/list.uvue'))], + ['OrderList', defineAsyncComponent(() => import('@/pages/mall/admin/order/order-management/index.uvue'))], ['OrderStatistic', defineAsyncComponent(() => import('@/pages/mall/admin/order/order-statistics/index.uvue'))], ['OrderRefund', defineAsyncComponent(() => import('@/pages/mall/admin/order/aftersales-order/index.uvue'))], ['OrderCashier', defineAsyncComponent(() => import('@/pages/mall/admin/order/cashier-order/index.uvue'))], diff --git a/pages/mall/admin/cms/article/list.uvue b/pages/mall/admin/cms/article/list.uvue index 60ac6697..56ce9fe6 100644 --- a/pages/mall/admin/cms/article/list.uvue +++ b/pages/mall/admin/cms/article/list.uvue @@ -5,18 +5,23 @@ 文章分类: - - {{ filterCategory }} - - + + + {{ categoryOptions[categoryIndex]?.label || '全部' }} + + + 文章搜索: - + 查询 + + 重置 + @@ -29,43 +34,57 @@ - ID - 文章图片 - 文章名称 - 关联商品 + 序号 + 封面图 + 文章标题 浏览量 - 时间 + 最后更新 + 状态 操作 - - {{ item.id }} + + 加载中... + + + 暂无文章数据 + + + {{ (page - 1) * pageSize + index + 1 }} - 🖼️ + + 无图 + + + + {{ item.title }} + 分类: {{ item.category_name }} + - {{ item.name }} - {{ item.linkedProduct }} {{ item.views }} - {{ item.time }} + {{ item.updated_at.substring(0, 16).replace('T', ' ') }} + + + + + 编辑 - 关联 - - 删除 - - 复制链接 - + 删除 - - 共 {{ articleList.length }} 条 + 共 {{ total }} 条 + + < + {{ page }} + > @@ -76,131 +95,64 @@ - 文章信息 + {{ isEdit ? '编辑文章' : '添加文章' }} × - - - - 文章信息 - - - - - - - *标题: - - - {{ formTitle.length }}/80 - - - - 作者: - - - {{ formAuthor.length }}/10 - + + *标题: + + - - - *文章分类: - + + *文章分类: + + - {{ formCategory || '请选择' }} + {{ categoryOptions[formCategoryIndex]?.label || '请选择分类' }} - - - - 文章简介: - -