admin模块接入数据库

This commit is contained in:
comlibmb
2026-02-13 17:29:50 +08:00
parent 56209b7a75
commit ec636dc703
58 changed files with 5586 additions and 1394 deletions

View 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 '管理员删除文章分类(含关联性检查)';

View 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;
$$;

View 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 '管理员新增或更新文章分类';

View File

@@ -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 '管理员设置文章分类状态';

View 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 '管理员删除文章记录';

View 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 '管理员获取文章完整详情';

View 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 '管理员分页查询文章列表';

View 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 '管理员新增或更新文章内容';

View 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 '管理员设置文章发布状态';

View 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;

View 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;