feat(admin): complete decoration module database integration including DIY pages, RLS and RPCs

This commit is contained in:
comlibmb
2026-02-16 22:20:43 +08:00
parent e1f48cc880
commit 7b27694690
11 changed files with 798 additions and 1169 deletions

View File

@@ -0,0 +1,51 @@
# 装修模块 (Decoration) 全量集成与数据库构建报告
## 摘要
本次对 Admin 侧装修模块Decoration Module进行了深度的端到端修复。补齐了该模块完全缺失的数据库表结构用于存储 DIY 布局 JSON、行级安全策略RLS以及 4 个管理端 RPC 接口。同时重构了前端 3 个核心页面,实现了从 Mock 数据到真实数据库持久化的闭环。
## 动机
装修模块允许管理员自定义商城的视觉表现(首页、专题页、个人中心风格)。此前该功能仅有 UI 静态模拟,无法保存任何配置。为了支撑个性化商城运营,必须补齐底层存储架构,并按照项目规范落地管理端接口。
## 影响范围
- **模块**:后台管理系统 - 装修模块
- **页面**:首页装修、商品分类装修、个人中心装修
- **接口**:新增 4 个 `rpc_admin_...` 接口
- **权限**:增加了 DIY 页面表的 RLS 策略,消费者端仅允许读取已启用配置
## 变更清单
### 数据库 SQL
- **新增 Schema** (docs/sql/10_schema/decoration/):
- `ak_diy_pages_v1.sql` (核心配置表)
- **新增 RLS** (docs/sql/20_rls/decoration/):
- `ml_decoration_rls_v1.sql`
- **新增 RPC** (docs/sql/30_rpc/decoration/):
- `rpc_admin_get_diy_page_list_v1.sql`
- `rpc_admin_save_diy_page_v1.sql`
- `rpc_admin_delete_diy_page_v1.sql`
- `rpc_admin_set_home_page_v1.sql`
### 前端代码
- **新增服务层**`services/admin/decorationService.uts` (封装全量 DIY 接口)
- **重构页面**
- `pages/mall/admin/decoration/home.uvue` (接入分页列表、删除及设为首页逻辑)
- `pages/mall/admin/decoration/user.uvue` (接入样式持久化存取)
- `pages/mall/admin/decoration/category.uvue` (接入样式持久化存取)
## 兼容性与风险
- **配置格式**`config` 字段采用 JSONB 存储,前端 UTS 解析时需确保类型强制转换正确(使用 `UTSJSONObject`)。
- **首页约束**:数据库侧实现了原子切换逻辑,确保同类型页面(如 `home`)全局仅有一个生效项。
## 回滚方案
1. **数据库**:依次 DROP `ak_diy_pages` 表及相关 4 个 RPC 函数。
2. **代码**:使用 `git checkout` 恢复重构的 3 个 `.uvue` 页面,并删除 `decorationService.uts`
## 验证方式
1. **功能验证**
- 首页切换:在“首页装修”列表中将另一个模板设为首页,确认移动端展示内容实时变更。
- 样式保存:在“个人中心装修”切换样式并点击保存,刷新页面后确认选中状态保持一致。
2. **安全验证**:确认非 admin 角色无法通过接口修改 DIY 配置。
## 关联规范
- 遵循 `AGENT_PROJECT_SPEC.md` 规范。
- 遵循统一的 RPC 入口鉴权admin 角色)。

View File

@@ -0,0 +1,32 @@
-- =====================================================================================
-- Schema: 装修模块 - DIY 页面配置表
-- 位置docs/sql/10_schema/decoration/ak_diy_pages_v1.sql
-- 对象类型TABLE
-- 版本v1
-- 说明:存储首页、专题页及个人中心的 DIY 布局 JSON 配置
-- =====================================================================================
CREATE TABLE IF NOT EXISTS public.ak_diy_pages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
type TEXT NOT NULL, -- home: 首页, topic: 专题页, user: 个人中心
config JSONB NOT NULL DEFAULT '{}'::jsonb, -- 核心布局配置 (组件列表及参数)
is_home BOOLEAN NOT NULL DEFAULT FALSE, -- 是否为生效首页
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES public.ak_users(id),
updated_by UUID REFERENCES public.ak_users(id)
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_diy_pages_type ON public.ak_diy_pages (type);
CREATE INDEX IF NOT EXISTS idx_diy_pages_is_home ON public.ak_diy_pages (is_home) WHERE is_home = TRUE;
-- 注释
COMMENT ON TABLE public.ak_diy_pages IS 'DIY 页面装修配置表';
COMMENT ON COLUMN public.ak_diy_pages.type IS '页面类型: home(首页), topic(专题), user(个人中心)';
COMMENT ON COLUMN public.ak_diy_pages.config IS 'DIY 布局配置 JSON';

View File

@@ -0,0 +1,18 @@
-- =====================================================================================
-- RLS: 装修模块 - DIY 页面安全策略
-- 位置docs/sql/20_rls/decoration/ml_decoration_rls_v1.sql
-- 对象类型RLS 策略
-- 版本v1
-- 说明:消费者端公开只读已发布的页面;管理端通过 SECURITY DEFINER RPC 进行管理
-- =====================================================================================
-- 1. 启用 RLS
ALTER TABLE public.ak_diy_pages ENABLE ROW LEVEL SECURITY;
-- 2. 消费者端策略:允许匿名和登录用户读取已启用的页面
DROP POLICY IF EXISTS diy_pages_select_active ON public.ak_diy_pages;
CREATE POLICY diy_pages_select_active ON public.ak_diy_pages
FOR SELECT TO anon, authenticated
USING (is_active = true);
-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行,此处不再额外开放直接表操作

View File

@@ -0,0 +1,40 @@
-- RPC: rpc_admin_delete_diy_page
-- 管理端删除 DIY 页面配置
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_diy_page(
p_id uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_ok boolean;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 2. 执行删除 (不允许删除当前生效的首页)
IF EXISTS (
SELECT 1 FROM public.ak_diy_pages
WHERE id = p_id AND is_home = true
) THEN
RAISE EXCEPTION 'cannot delete the active home page';
END IF;
DELETE FROM public.ak_diy_pages WHERE id = p_id;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_diy_page(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_diy_page(uuid) TO authenticated;

View File

@@ -0,0 +1,60 @@
-- RPC: rpc_admin_get_diy_page_list
-- 管理端获取 DIY 页面分页列表
-- 支持按名称搜索和按类型筛选
CREATE OR REPLACE FUNCTION public.rpc_admin_get_diy_page_list(
p_search text DEFAULT NULL,
p_type text DEFAULT NULL,
p_page integer DEFAULT 1,
p_page_size integer DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_page integer := GREATEST(1, COALESCE(p_page, 1));
v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
v_offset integer := (v_page - 1) * v_page_size;
v_total bigint;
v_items jsonb;
BEGIN
-- 1. 权限检查 (仅管理员或分析员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ak_diy_pages
WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%')
AND (p_type IS NULL OR type = p_type);
-- 3. 获取明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, name, type, is_home, is_active,
created_at, updated_at
FROM public.ak_diy_pages
WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%')
AND (p_type IS NULL OR type = p_type)
ORDER BY created_at DESC
LIMIT v_page_size OFFSET v_offset
) t;
-- 4. 返回 JSON 结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_diy_page_list(text, text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_diy_page_list(text, text, integer, integer) TO authenticated;

View File

@@ -0,0 +1,57 @@
-- RPC: rpc_admin_save_diy_page
-- 管理端新增或更新 DIY 页面配置
CREATE OR REPLACE FUNCTION public.rpc_admin_save_diy_page(
p_id uuid DEFAULT NULL,
p_name text DEFAULT NULL,
p_type text DEFAULT NULL,
p_config jsonb DEFAULT '{}'::jsonb,
p_is_active boolean DEFAULT true
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_id uuid;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 2. 新增或更新
IF p_id IS NULL THEN
INSERT INTO public.ak_diy_pages (
name, type, config, is_active, updated_by, created_by
) VALUES (
p_name, p_type, p_config, p_is_active, auth.uid(), auth.uid()
) RETURNING id INTO v_id;
ELSE
UPDATE public.ak_diy_pages
SET
name = COALESCE(p_name, name),
type = COALESCE(p_type, type),
config = COALESCE(p_config, config),
is_active = COALESCE(p_is_active, is_active),
updated_at = now(),
updated_by = auth.uid()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'page not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_diy_page(uuid, text, text, jsonb, boolean) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_diy_page(uuid, text, text, jsonb, boolean) TO authenticated;

View File

@@ -0,0 +1,40 @@
-- RPC: rpc_admin_set_home_page
-- 管理端设置生效首页
-- 逻辑:先取消所有同类型页面的 is_home 状态,再设置目标页面为 is_home
CREATE OR REPLACE FUNCTION public.rpc_admin_set_home_page(
p_id uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_type text;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 2. 获取目标页面类型
SELECT type INTO v_type FROM public.ak_diy_pages WHERE id = p_id;
IF v_type IS NULL THEN
RAISE EXCEPTION 'page not found';
END IF;
-- 3. 原子切换:同一类型的页面只能有一个 is_home
UPDATE public.ak_diy_pages SET is_home = false WHERE type = v_type;
UPDATE public.ak_diy_pages SET is_home = true WHERE id = p_id;
RETURN true;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_set_home_page(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_set_home_page(uuid) TO authenticated;