Files
medical-mall/docs/sql/all_rpc_merged.sql

6170 lines
192 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =====================================================================================
-- Admin 统计功能 - 获取全站核心指标概览 RPC
-- 位置docs/sql/30_rpc/admin/rpc_admin_get_overall_stats_v1.sql
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:一次性聚合查询销售、订单、用户及商品的核心统计指标
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_get_overall_stats()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_stats JSONB;
v_today_start TIMESTAMPTZ := CURRENT_DATE;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 聚合统计
WITH totals AS (
SELECT
(SELECT COALESCE(SUM(paid_amount), 0) FROM public.ml_orders WHERE paid = 1) as total_sales,
(SELECT COUNT(*) FROM public.ml_orders WHERE paid = 1) as total_orders,
(SELECT COUNT(*) FROM public.ak_users) as total_users,
(SELECT COUNT(*) FROM public.ml_products) as total_products
),
today_stats AS (
SELECT
(SELECT COALESCE(SUM(paid_amount), 0) FROM public.ml_orders WHERE paid = 1 AND created_at >= v_today_start) as today_sales,
(SELECT COUNT(*) FROM public.ml_orders WHERE paid = 1 AND created_at >= v_today_start) as today_orders,
(SELECT COUNT(*) FROM public.ak_users WHERE created_at >= v_today_start) as today_new_users
),
pending_tasks AS (
SELECT
(SELECT COUNT(*) FROM public.ml_orders WHERE paid = 1 AND order_status = 1) as pending_delivery,
(SELECT COUNT(*) FROM public.ml_product_skus WHERE stock <= 10) as stock_warning, -- 假设库存小于10为预警
(SELECT COUNT(*) FROM public.ml_extract WHERE status = 0) as pending_extract
)
SELECT jsonb_build_object(
'totals', (SELECT row_to_json(totals.*) FROM totals),
'today', (SELECT row_to_json(today_stats.*) FROM today_stats),
'pending', (SELECT row_to_json(pending_tasks.*) FROM pending_tasks)
) INTO v_stats;
RETURN v_stats;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_overall_stats() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_overall_stats() TO authenticated;
-- =====================================================================================
-- Admin 系统维护 - 获取服务器环境信息 RPC
-- 位置docs/sql/30_rpc/admin/rpc_admin_get_system_info_v1.sql
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:获取服务器操作系统、数据库版本及运行环境信息
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_get_system_info()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_info JSONB;
v_db_version TEXT;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取数据库版本
SELECT version() INTO v_db_version;
-- 3. 构建返回信息
v_info := jsonb_build_object(
'server_os', 'Linux (Simulated)', -- 数据库侧通常难以直接获取完整的宿主系统信息
'web_server', 'Nginx/1.24.0 (Simulated)',
'db_engine', 'PostgreSQL',
'db_version', v_db_version,
'uts_runtime', 'uni-app x (UTS)',
'auth_id', 'ZC2884891' -- 模拟授权码
);
RETURN v_info;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_system_info() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_system_info() TO authenticated;
-- =====================================================================================
-- Admin 系统功能 - 获取配置项 RPC
-- 位置docs/sql/30_rpc/admin/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_system_configs, ak_users 表已存在
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_system_config_get(
p_key TEXT
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_value JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取配置值
SELECT config_value INTO v_value
FROM public.ml_system_configs
WHERE config_key = p_key;
RETURN v_value;
END;
$$;-- =====================================================================================
-- Admin 系统功能 - 保存/更新配置项 RPC
-- 位置docs/sql/30_rpc/admin/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_system_configs, ak_users 表已存在
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_system_config_save(
p_key TEXT,
p_value JSONB,
p_description TEXT DEFAULT NULL
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 插入或更新配置
INSERT INTO public.ml_system_configs (config_key, config_value, description, updated_at)
VALUES (p_key, p_value, p_description, NOW())
ON CONFLICT (config_key) DO UPDATE
SET
config_value = EXCLUDED.config_value,
description = COALESCE(EXCLUDED.description, public.ml_system_configs.description),
updated_at = NOW();
RETURN TRUE;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_system_config_save(TEXT, JSONB, TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_system_config_save(TEXT, JSONB, TEXT) TO authenticated;
-- =====================================================================================
-- RPC: rpc_analytics_user_gender_distribution
-- Version: v1
-- Purpose: 统计指定周期内新增用户的性别分布(用于 Admin/Analytics 图表)
-- Security: SECURITY DEFINER + 固定 search_path + 入口角色鉴权
-- Depends: public.ak_users, public.get_current_user_role()
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_analytics_user_gender_distribution(
p_start_date DATE,
p_end_date DATE
)
RETURNS TABLE (
name TEXT,
value BIGINT
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
IF public.get_current_user_role() NOT IN ('admin', 'analytics') THEN
RAISE EXCEPTION 'Permission denied: required role admin or analytics';
END IF;
RETURN QUERY
SELECT
CASE
WHEN gender IS NULL OR TRIM(gender::text) = '' THEN '未知'
WHEN LOWER(TRIM(gender::text)) = 'male' THEN ''
WHEN LOWER(TRIM(gender::text)) = 'female' THEN ''
WHEN LOWER(TRIM(gender::text)) = 'other' THEN '未知'
ELSE '未知'
END AS name,
COUNT(*)::BIGINT AS value
FROM public.ak_users
WHERE created_at::DATE BETWEEN p_start_date AND p_end_date
GROUP BY 1
ORDER BY value DESC;
END;
$$;
-- =====================================================================================
-- 函数: check_admin_permission
-- 描述: 通用的 RBAC 权限校验函数
-- 参数: p_permission_code - 权限编码 (如 'role:delete', 'user:view')
-- 返回: BOOLEAN
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.check_admin_permission(
p_permission_code TEXT DEFAULT NULL
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id UUID;
v_role TEXT;
BEGIN
-- 1. 获取当前登录用户的 Profile ID 和角色
SELECT id, role INTO v_user_id, v_role
FROM public.ak_users
WHERE auth_id = auth.uid();
-- 2. 未登录或未找到 Profile
IF v_user_id IS NULL THEN
RETURN FALSE;
END IF;
-- 3. 超级管理员拥有所有权限 (保持向下兼容)
IF v_role = 'admin' THEN
RETURN TRUE;
END IF;
-- 4. 如果指定了权限编码,则检查 ak_permissions 体系
IF p_permission_code IS NOT NULL THEN
RETURN EXISTS (
SELECT 1
FROM public.ak_admin_roles ar
JOIN public.ak_role_permissions rp ON ar.role_id = rp.role_id
JOIN public.ak_permissions p ON rp.permission_id = p.id
WHERE ar.admin_id = v_user_id
AND p.code = p_permission_code
AND p.deleted_at IS NULL
AND ar.deleted_at IS NULL
);
END IF;
RETURN FALSE;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.check_admin_permission(TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.check_admin_permission(TEXT) TO authenticated;
-- =====================================================================================
-- RPC: get_current_user_role
-- Version: v1
-- Purpose: 获取当前登录用户的角色(用于 RPC 入口鉴权)
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends: public.ak_users (auth_id, role)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.get_current_user_role()
RETURNS TEXT
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
AS $$
SELECT role
FROM public.ak_users
WHERE auth_id = auth.uid()
LIMIT 1;
$$;-- =====================================================================================
-- Trigger Function: handle_new_user
-- Version: v2
-- Purpose: auth.users 新用户创建后,同步写入 public.ak_users权威用户表并保持 user_roles 兼容写入
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends:
-- - public.ak_users(auth_id,email,username,role)
-- - public.user_roles(user_id,role,created_by) (如存在)
-- Notes:
-- - 角色权威口径为 public.ak_users.role
-- - user_roles 为历史/兼容表:存在则写入,不存在则跳过
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
user_role TEXT := 'customer';
user_email TEXT := NEW.email;
user_name TEXT;
has_user_roles BOOLEAN := FALSE;
BEGIN
-- 1) 基于邮箱规则分配默认角色(可按需调整)
IF user_email ILIKE '%@teacher.%' OR user_email ILIKE '%@edu.%' THEN
user_role := 'teacher';
ELSIF user_email ILIKE '%@admin.%' THEN
user_role := 'admin';
END IF;
-- 2) 默认 username取邮箱 @ 前缀
IF user_email IS NOT NULL AND POSITION('@' IN user_email) > 1 THEN
user_name := SPLIT_PART(user_email, '@', 1);
ELSE
user_name := 'user';
END IF;
-- 3) 写入 ak_users权威
-- 使用 ON CONFLICT 确保幂等:同一 auth_id 只会有一条记录
INSERT INTO public.ak_users (auth_id, email, username, role)
VALUES (NEW.id, user_email, user_name, user_role)
ON CONFLICT (auth_id)
DO UPDATE SET
email = COALESCE(EXCLUDED.email, public.ak_users.email),
username = COALESCE(EXCLUDED.username, public.ak_users.username),
role = COALESCE(public.ak_users.role, EXCLUDED.role),
updated_at = now();
-- 4) 兼容写入 user_roles如果表存在
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema='public'
AND table_name='user_roles'
) INTO has_user_roles;
IF has_user_roles THEN
BEGIN
INSERT INTO public.user_roles (user_id, role, created_by)
VALUES (NEW.id, user_role, NEW.id);
EXCEPTION WHEN unique_violation THEN
-- 忽略重复
NULL;
END;
END IF;
-- 5) 更新 auth.users 元数据(可选保留)
UPDATE auth.users
SET raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) || jsonb_build_object('user_role', user_role)
WHERE id = NEW.id;
RETURN NEW;
END;
$$;
-- =====================================================================================
-- Trigger Function: handle_new_user
-- Version: v3
-- Purpose: auth.users 新用户创建后,同步写入 public.ak_users权威和 public.user_roles兼容
-- 此版本修复了向 user_roles 写入时可能因 role 为 NULL 导致的 NOT NULL 约束失败问题。
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends:
-- - public.ak_users(auth_id,email,username,role)
-- - public.user_roles(user_id,role)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
user_role TEXT;
user_email TEXT := NEW.email;
user_name TEXT;
has_user_roles BOOLEAN := FALSE;
BEGIN
-- 1) 基于邮箱规则分配默认角色(可按需调整)
-- 确保 user_role 总有一个非 NULL 的值
user_role := CASE
WHEN user_email ILIKE '%@admin.%' THEN 'admin'
WHEN user_email ILIKE '%@teacher.%' OR user_email ILIKE '%@edu.%' THEN 'teacher'
ELSE 'consumer' -- 默认角色
END;
-- 2) 默认 username取邮箱 @ 前缀
IF user_email IS NOT NULL AND POSITION('@' IN user_email) > 1 THEN
user_name := SPLIT_PART(user_email, '@', 1);
ELSE
user_name := 'user_' || SUBSTRING(NEW.id::text, 1, 8); -- 使用 user_ + uid前8位作为备用名
END IF;
-- 3) 写入 ak_users权威
-- 使用 ON CONFLICT 确保幂等:同一 auth_id 只会有一条记录
INSERT INTO public.ak_users (auth_id, email, username, role)
VALUES (NEW.id, user_email, user_name, user_role)
ON CONFLICT (auth_id)
DO UPDATE SET
email = COALESCE(EXCLUDED.email, public.ak_users.email),
username = COALESCE(EXCLUDED.username, public.ak_users.username),
-- 只有当现有 role 为空时才更新,避免覆盖手动设置的 admin 角色
role = COALESCE(public.ak_users.role, EXCLUDED.role),
updated_at = now();
-- 4) 兼容写入 user_roles如果表存在
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema='public'
AND table_name='user_roles'
) INTO has_user_roles;
IF has_user_roles THEN
BEGIN
-- 确保插入的 role 不为 NULL即使上面的逻辑有误
INSERT INTO public.user_roles (user_id, role, created_by)
VALUES (NEW.id, COALESCE(user_role, 'customer'), NEW.id);
EXCEPTION
WHEN unique_violation THEN
-- 忽略重复插入的错误
NULL;
WHEN not_null_violation THEN
-- 记录非空约束错误,但不中断整个触发器
RAISE NOTICE '[handle_new_user] WARNING: Failed to INSERT into user_roles due to NOT NULL violation. user_id: %, role: %', NEW.id, user_role;
END;
END IF;
-- 5) 更新 auth.users 元数据(可选保留)
UPDATE auth.users
SET raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) || jsonb_build_object('user_role', user_role)
WHERE id = NEW.id;
RETURN NEW;
END;
$$;-- =====================================================================================
-- Trigger Function: handle_new_user
-- Version: v4
-- Purpose: auth.users 新用户创建后,优先读取 raw_user_meta_data.user_role 写入 ak_users.role。
-- 解决 delivery / merchant 注册时被错误降级为默认 consumer 的问题。
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends:
-- - public.ak_users(auth_id,email,username,role)
-- - public.user_roles(user_id,role)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
user_role TEXT;
metadata_role TEXT;
user_email TEXT := NEW.email;
user_name TEXT;
ak_user_id UUID;
has_user_roles BOOLEAN := FALSE;
has_delivery_staff BOOLEAN := FALSE;
BEGIN
metadata_role := NULLIF(TRIM(COALESCE(NEW.raw_user_meta_data ->> 'user_role', '')), '');
user_role := CASE
WHEN metadata_role IN ('customer', 'merchant', 'delivery', 'service', 'admin') THEN metadata_role
WHEN user_email ILIKE '%@admin.%' THEN 'admin'
WHEN user_email ILIKE '%@teacher.%' OR user_email ILIKE '%@edu.%' THEN 'customer'
ELSE 'customer'
END;
IF user_email IS NOT NULL AND POSITION('@' IN user_email) > 1 THEN
user_name := SPLIT_PART(user_email, '@', 1);
ELSE
user_name := 'user_' || SUBSTRING(NEW.id::text, 1, 8);
END IF;
INSERT INTO public.ak_users (auth_id, email, username, role)
VALUES (NEW.id, user_email, user_name, user_role)
ON CONFLICT (auth_id)
DO UPDATE SET
email = COALESCE(EXCLUDED.email, public.ak_users.email),
username = COALESCE(EXCLUDED.username, public.ak_users.username),
role = COALESCE(NULLIF(public.ak_users.role, ''), EXCLUDED.role),
updated_at = now()
RETURNING id INTO ak_user_id;
IF user_role = 'delivery' THEN
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'ml_delivery_staff'
) INTO has_delivery_staff;
IF has_delivery_staff THEN
INSERT INTO public.ml_delivery_staff (
uid,
nickname,
phone,
status,
is_active
)
SELECT
ak_user_id,
user_name,
'',
1,
TRUE
WHERE NOT EXISTS (
SELECT 1
FROM public.ml_delivery_staff
WHERE uid = ak_user_id
);
END IF;
END IF;
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'user_roles'
) INTO has_user_roles;
IF has_user_roles THEN
BEGIN
INSERT INTO public.user_roles (user_id, role, created_by)
VALUES (NEW.id, user_role, NEW.id)
ON CONFLICT DO NOTHING;
EXCEPTION
WHEN check_violation THEN
RAISE NOTICE '[handle_new_user_v4] WARNING: Skipped user_roles insert due to check violation. user_id: %, role: %', NEW.id, user_role;
WHEN not_null_violation THEN
RAISE NOTICE '[handle_new_user_v4] WARNING: Failed to INSERT into user_roles due to NOT NULL violation. user_id: %, role: %', NEW.id, user_role;
WHEN others THEN
RAISE NOTICE '[handle_new_user_v4] WARNING: Skipped user_roles insert due to unexpected error. user_id: %, role: %, err: %', NEW.id, user_role, SQLERRM;
END;
END IF;
UPDATE auth.users
SET raw_user_meta_data = COALESCE(raw_user_meta_data, '{}'::jsonb) || jsonb_build_object('user_role', user_role)
WHERE id = NEW.id;
RETURN NEW;
END;
$$;-- RPC: rpc_admin_delete_permission
-- 管理端删除功能权限/菜单(支持级联软删除关联的角色权限映射)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_permission(
p_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查 (使用通用权限校验函数,权限编码: permission:delete)
IF NOT public.check_admin_permission('permission:delete') THEN
RAISE EXCEPTION 'Permission denied: permission:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id
FROM public.ak_users
WHERE auth_id = auth.uid();
-- 3. 级联软删除:先删除所有关联了该权限的角色映射
UPDATE public.ak_role_permissions
SET deleted_at = now(),
deleted_by = v_user_id
WHERE permission_id = p_id AND deleted_at IS NULL;
-- 4. 最后软删除权限本身
UPDATE public.ak_permissions
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_permission(UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_permission(UUID) TO authenticated;
-- RPC: rpc_admin_delete_role
-- 管理端删除角色(支持级联软删除关联权限)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_role(
p_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查 (使用通用权限校验函数,权限编码: role:delete)
IF NOT public.check_admin_permission('role:delete') THEN
RAISE EXCEPTION 'Permission denied: role:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id
FROM public.ak_users
WHERE auth_id = auth.uid();
-- 3. 级联软删除:先删除该角色下的所有权限关联
UPDATE public.ak_role_permissions
SET deleted_at = now(),
deleted_by = v_user_id
WHERE role_id = p_id AND deleted_at IS NULL;
-- 4. 级联软删除:再删除该角色下的所有管理员关联
UPDATE public.ak_admin_roles
SET deleted_at = now(),
deleted_by = v_user_id
WHERE role_id = p_id AND deleted_at IS NULL;
-- 5. 最后软删除角色本身
UPDATE public.ak_roles
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_role(UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_role(UUID) TO authenticated;
-- RPC: rpc_admin_get_admin_list
-- 管理端获取管理员列表
-- 筛选 ak_users 表中 role 为 'admin' 或 'analytics' 的用户,并关联显示其角色信息
CREATE OR REPLACE FUNCTION public.rpc_admin_get_admin_list(
p_search TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT NULL,
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_offset INTEGER := (p_page - 1) * p_page_size;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ak_users u
WHERE u.role IN ('admin', 'analytics')
AND (p_status IS NULL OR u.is_active = (p_status = 1))
AND (p_search IS NULL OR u.username ILIKE '%' || p_search || '%' OR u.real_name ILIKE '%' || p_search || '%');
-- 3. 获取数据列表 (关联角色)
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
u.id,
u.username,
u.real_name,
u.role,
u.is_active,
u.last_login_at,
u.last_login_ip,
(
SELECT jsonb_agg(r.name)
FROM public.ak_admin_roles ar
JOIN public.ak_roles r ON r.id = ar.role_id
WHERE ar.user_id = u.id
) as roles
FROM public.ak_users u
WHERE u.role IN ('admin', 'analytics')
AND (p_status IS NULL OR u.is_active = (p_status = 1))
AND (p_search IS NULL OR u.username ILIKE '%' || p_search || '%' OR u.real_name ILIKE '%' || p_search || '%')
ORDER BY u.created_at DESC
LIMIT p_page_size OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_admin_list(TEXT, SMALLINT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_admin_list(TEXT, SMALLINT, INTEGER, INTEGER) TO authenticated;
-- RPC: rpc_admin_get_permission_list
-- 管理端获取全量权限/菜单列表 (供前端构建树形结构)
CREATE OR REPLACE FUNCTION public.rpc_admin_get_permission_list()
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取全量数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, parent_id, name, code, type,
path, icon, sort_order, is_visible,
created_at, updated_at
FROM public.ak_permissions
WHERE deleted_at IS NULL
ORDER BY sort_order ASC, created_at ASC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_permission_list() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_permission_list() TO authenticated;
-- RPC: rpc_admin_get_role_list
-- 管理端获取角色分页列表
CREATE OR REPLACE FUNCTION public.rpc_admin_get_role_list(
p_search TEXT DEFAULT NULL,
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_offset INTEGER := (p_page - 1) * p_page_size;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ak_roles
WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%' OR code ILIKE '%' || p_search || '%');
-- 3. 获取明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, name, code, description, is_active,
created_at, updated_at
FROM public.ak_roles
WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%' OR code ILIKE '%' || p_search || '%')
ORDER BY created_at DESC
LIMIT p_page_size OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_role_list(TEXT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_role_list(TEXT, INTEGER, INTEGER) TO authenticated;
-- RPC: rpc_admin_save_permission
-- 管理端新增或更新功能权限/菜单
CREATE OR REPLACE FUNCTION public.rpc_admin_save_permission(
p_id UUID DEFAULT NULL,
p_parent_id UUID DEFAULT NULL,
p_name TEXT DEFAULT NULL,
p_code TEXT DEFAULT NULL,
p_type TEXT DEFAULT 'menu',
p_path TEXT DEFAULT NULL,
p_icon TEXT DEFAULT NULL,
p_sort_order INTEGER DEFAULT 0,
p_is_visible BOOLEAN DEFAULT TRUE
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 新增
IF p_id IS NULL THEN
IF p_name IS NULL OR p_code IS NULL THEN
RAISE EXCEPTION 'Missing required fields: name or code';
END IF;
INSERT INTO public.ak_permissions (
parent_id, name, code, type, path, icon, sort_order, is_visible
) VALUES (
p_parent_id, p_name, p_code, p_type, p_path, p_icon, p_sort_order, p_is_visible
) RETURNING id INTO v_id;
ELSE
-- 3. 更新
UPDATE public.ak_permissions
SET
parent_id = COALESCE(p_parent_id, parent_id),
name = COALESCE(p_name, name),
code = COALESCE(p_code, code),
type = COALESCE(p_type, type),
path = COALESCE(p_path, path),
icon = COALESCE(p_icon, icon),
sort_order = COALESCE(p_sort_order, sort_order),
is_visible = COALESCE(p_is_visible, is_visible),
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Permission item not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_permission(UUID, UUID, TEXT, TEXT, TEXT, TEXT, TEXT, INTEGER, BOOLEAN) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_permission(UUID, UUID, TEXT, TEXT, TEXT, TEXT, TEXT, INTEGER, BOOLEAN) TO authenticated;
-- RPC: rpc_admin_save_role
-- 管理端新增或更新角色
CREATE OR REPLACE FUNCTION public.rpc_admin_save_role(
p_id UUID DEFAULT NULL,
p_name TEXT DEFAULT NULL,
p_code TEXT DEFAULT NULL,
p_description TEXT DEFAULT NULL,
p_is_active BOOLEAN DEFAULT TRUE
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 新增
IF p_id IS NULL THEN
IF p_name IS NULL OR p_code IS NULL THEN
RAISE EXCEPTION 'Missing required fields: name or code';
END IF;
INSERT INTO public.ak_roles (
name, code, description, is_active
) VALUES (
p_name, p_code, p_description, p_is_active
) RETURNING id INTO v_id;
ELSE
-- 3. 更新
UPDATE public.ak_roles
SET
name = COALESCE(p_name, name),
code = COALESCE(p_code, code),
description = COALESCE(p_description, description),
is_active = COALESCE(p_is_active, is_active),
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Role not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_role(UUID, TEXT, TEXT, TEXT, BOOLEAN) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_role(UUID, TEXT, TEXT, TEXT, BOOLEAN) TO authenticated;
-- =====================================================================================
-- RPC: rpc_admin_article_category_delete
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除文章分类(支持级联软删除分类下的文章)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('cms:category:delete') THEN
RAISE EXCEPTION 'Permission denied: cms:category:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 级联软删除:该分类下的所有文章
UPDATE public.ml_articles
SET deleted_at = now(),
deleted_by = v_user_id
WHERE category_id = p_id AND deleted_at IS NULL;
-- 4. 软删除分类本身
UPDATE public.ml_article_categories
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_category_delete IS '管理员删除文章分类(级联软删除关联文章)';
-- =====================================================================================
-- RPC: rpc_admin_article_category_list
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取文章分类列表
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_article_categories
WHERE (p_search IS NULL OR name ILIKE '%' || p_search || '%');
-- 3. 获取列表
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT id, name, icon, sort, status, created_at, updated_at
FROM public.ml_article_categories
WHERE (p_search IS NULL OR name ILIKE '%' || p_search || '%')
ORDER BY sort ASC, created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_article_category_save
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新文章分类
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_save(
p_id UUID DEFAULT NULL,
p_name TEXT DEFAULT NULL,
p_icon TEXT DEFAULT NULL,
p_sort INTEGER DEFAULT 0,
p_status SMALLINT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 参数校验
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
RAISE EXCEPTION 'Invalid name';
END IF;
-- 3. 新增
IF p_id IS NULL THEN
INSERT INTO public.ml_article_categories (
name, icon, sort, status
) VALUES (
p_name, p_icon, p_sort, p_status
) RETURNING id INTO v_id;
ELSE
-- 4. 更新
UPDATE public.ml_article_categories
SET
name = p_name,
icon = COALESCE(p_icon, icon),
sort = p_sort,
status = p_status,
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Category not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_category_save IS '管理员新增或更新文章分类';
-- =====================================================================================
-- RPC: rpc_admin_article_category_set_status
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端切换文章分类启用/禁用状态
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_set_status(
p_id UUID,
p_status SMALLINT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 更新状态
UPDATE public.ml_article_categories
SET status = p_status,
updated_at = now()
WHERE id = p_id;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_category_set_status IS '管理员设置文章分类状态';
-- =====================================================================================
-- RPC: rpc_admin_article_delete
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除文章记录(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('cms:article:delete') THEN
RAISE EXCEPTION 'Permission denied: cms:article:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除
UPDATE public.ml_articles
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_delete IS '管理员删除文章记录';
-- =====================================================================================
-- RPC: rpc_admin_article_get_detail
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端获取指定文章的完整详情
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_get_detail(
p_id UUID
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_item JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取详情
SELECT jsonb_build_object(
'id', a.id,
'category_id', a.category_id,
'category_name', c.name,
'title', a.title,
'author', a.author,
'image', a.image,
'description', a.description,
'content', a.content,
'status', a.status,
'views', a.views,
'is_banner', a.is_banner,
'is_hot', a.is_hot,
'linked_product_id', a.linked_product_id,
'created_at', a.created_at,
'updated_at', a.updated_at
) INTO v_item
FROM public.ml_articles a
LEFT JOIN public.ml_article_categories c ON c.id = a.category_id
WHERE a.id = p_id;
IF v_item IS NULL THEN
RAISE EXCEPTION 'Article not found';
END IF;
RETURN v_item;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_get_detail IS '管理员获取文章完整详情';
-- =====================================================================================
-- RPC: rpc_admin_article_list
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取文章列表,支持搜索、分类筛选及状态过滤
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_category_id UUID DEFAULT NULL,
p_status SMALLINT DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_articles a
WHERE (p_category_id IS NULL OR a.category_id = p_category_id)
AND (p_status IS NULL OR a.status = p_status)
AND (p_search IS NULL OR a.title ILIKE '%' || p_search || '%' OR a.author ILIKE '%' || p_search || '%');
-- 3. 获取列表数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
a.id,
a.category_id,
c.name as category_name,
a.title,
a.author,
a.image,
a.description,
a.status,
a.views,
a.is_banner,
a.is_hot,
a.created_at,
a.updated_at
FROM public.ml_articles a
LEFT JOIN public.ml_article_categories c ON c.id = a.category_id
WHERE (p_category_id IS NULL OR a.category_id = p_category_id)
AND (p_status IS NULL OR a.status = p_status)
AND (p_search IS NULL OR a.title ILIKE '%' || p_search || '%' OR a.author ILIKE '%' || p_search || '%')
ORDER BY a.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_list IS '管理员分页查询文章列表';
-- =====================================================================================
-- RPC: rpc_admin_article_save
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新文章内容
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_save(
p_id UUID DEFAULT NULL,
p_category_id UUID DEFAULT NULL,
p_title TEXT DEFAULT NULL,
p_author TEXT DEFAULT NULL,
p_image TEXT DEFAULT NULL,
p_description TEXT DEFAULT NULL,
p_content TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT 0,
p_is_banner BOOLEAN DEFAULT FALSE,
p_is_hot BOOLEAN DEFAULT FALSE,
p_linked_product_id UUID DEFAULT NULL
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 参数校验
IF p_title IS NULL OR length(trim(p_title)) = 0 THEN
RAISE EXCEPTION 'Invalid title';
END IF;
IF p_category_id IS NULL THEN
RAISE EXCEPTION 'Category is required';
END IF;
-- 3. 新增
IF p_id IS NULL THEN
INSERT INTO public.ml_articles (
category_id, title, author, image, description, content,
status, is_banner, is_hot, linked_product_id
) VALUES (
p_category_id, p_title, p_author, p_image, p_description, p_content,
p_status, p_is_banner, p_is_hot, p_linked_product_id
) RETURNING id INTO v_id;
ELSE
-- 4. 更新
UPDATE public.ml_articles
SET
category_id = COALESCE(p_category_id, category_id),
title = COALESCE(p_title, title),
author = COALESCE(p_author, author),
image = COALESCE(p_image, image),
description = COALESCE(p_description, description),
content = COALESCE(p_content, content),
status = COALESCE(p_status, status),
is_banner = COALESCE(p_is_banner, is_banner),
is_hot = COALESCE(p_is_hot, is_hot),
linked_product_id = p_linked_product_id,
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Article not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_save IS '管理员新增或更新文章内容';
-- =====================================================================================
-- RPC: rpc_admin_article_set_status
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端切换文章发布/下架状态
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_set_status(
p_id UUID,
p_status SMALLINT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 更新状态
UPDATE public.ml_articles
SET status = p_status,
updated_at = now()
WHERE id = p_id;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_set_status IS '管理员设置文章发布状态';
-- RPC: rpc_admin_delete_diy_page
-- 管理端删除 DIY 页面配置(支持权限检查与首页保护)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_diy_page(
p_id uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_ok boolean;
v_user_id UUID;
BEGIN
-- 1. 权限检查 (使用通用权限校验函数)
IF NOT public.check_admin_permission('decoration:page:delete') THEN
RAISE EXCEPTION 'Permission denied: decoration:page:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 校验:不允许删除当前生效的首页
IF EXISTS (
SELECT 1 FROM public.ak_diy_pages
WHERE id = p_id AND is_home = true AND deleted_at IS NULL
) THEN
RAISE EXCEPTION 'cannot delete the active home page';
END IF;
-- 4. 执行软删除:标记 deleted_at
UPDATE public.ak_diy_pages
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_diy_page(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_diy_page(uuid) TO authenticated;
-- RPC: rpc_admin_get_diy_page_list
-- 管理端获取 DIY 页面分页列表
-- 支持按名称搜索和按类型筛选
CREATE OR REPLACE FUNCTION public.rpc_admin_get_diy_page_list(
p_search text DEFAULT NULL,
p_type text DEFAULT NULL,
p_page integer DEFAULT 1,
p_page_size integer DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_page integer := GREATEST(1, COALESCE(p_page, 1));
v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
v_offset integer := (v_page - 1) * v_page_size;
v_total bigint;
v_items jsonb;
BEGIN
-- 1. 权限检查 (仅管理员或分析员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ak_diy_pages
WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%')
AND (p_type IS NULL OR type = p_type);
-- 3. 获取明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, name, type, is_home, is_active,
created_at, updated_at
FROM public.ak_diy_pages
WHERE (p_search IS NULL OR p_search = '' OR name ILIKE '%' || p_search || '%')
AND (p_type IS NULL OR type = p_type)
ORDER BY created_at DESC
LIMIT v_page_size OFFSET v_offset
) t;
-- 4. 返回 JSON 结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_diy_page_list(text, text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_diy_page_list(text, text, integer, integer) TO authenticated;
-- RPC: rpc_admin_save_diy_page
-- 管理端新增或更新 DIY 页面配置
CREATE OR REPLACE FUNCTION public.rpc_admin_save_diy_page(
p_id uuid DEFAULT NULL,
p_name text DEFAULT NULL,
p_type text DEFAULT NULL,
p_config jsonb DEFAULT '{}'::jsonb,
p_is_active boolean DEFAULT true
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_id uuid;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 2. 新增或更新
IF p_id IS NULL THEN
INSERT INTO public.ak_diy_pages (
name, type, config, is_active, updated_by, created_by
) VALUES (
p_name, p_type, p_config, p_is_active, auth.uid(), auth.uid()
) RETURNING id INTO v_id;
ELSE
UPDATE public.ak_diy_pages
SET
name = COALESCE(p_name, name),
type = COALESCE(p_type, type),
config = COALESCE(p_config, config),
is_active = COALESCE(p_is_active, is_active),
updated_at = now(),
updated_by = auth.uid()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'page not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_diy_page(uuid, text, text, jsonb, boolean) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_diy_page(uuid, text, text, jsonb, boolean) TO authenticated;
-- RPC: rpc_admin_set_home_page
-- 管理端设置生效首页
-- 逻辑:先取消所有同类型页面的 is_home 状态,再设置目标页面为 is_home
CREATE OR REPLACE FUNCTION public.rpc_admin_set_home_page(
p_id uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_type text;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 2. 获取目标页面类型
SELECT type INTO v_type FROM public.ak_diy_pages WHERE id = p_id;
IF v_type IS NULL THEN
RAISE EXCEPTION 'page not found';
END IF;
-- 3. 原子切换:同一类型的页面只能有一个 is_home
UPDATE public.ak_diy_pages SET is_home = false WHERE type = v_type;
UPDATE public.ak_diy_pages SET is_home = true WHERE id = p_id;
RETURN true;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_set_home_page(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_set_home_page(uuid) TO authenticated;
-- RPC: rpc_admin_delete_delivery_staff
-- 管理端删除配送员(支持权限检查)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_delivery_staff(
p_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查 (使用通用权限校验函数)
IF NOT public.check_admin_permission('delivery:staff:delete') THEN
RAISE EXCEPTION 'Permission denied: delivery:staff:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除:标记 deleted_at
UPDATE public.ml_delivery_staff
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_delivery_staff(UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_delivery_staff(UUID) TO authenticated;
-- RPC: rpc_admin_delete_delivery_station
-- 管理端删除提货点/核销点(支持级联软删除配送员关联)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_delivery_station(
p_id UUID
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('delivery:station:delete') THEN
RAISE EXCEPTION 'Permission denied: delivery:station:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 级联软删除:该站点下的所有配送员
UPDATE public.ml_delivery_staff
SET deleted_at = now(),
deleted_by = v_user_id
WHERE station_id = p_id AND deleted_at IS NULL;
-- 4. 执行软删除站点本身
UPDATE public.ml_delivery_stations
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_delete_delivery_station(UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_delivery_station(UUID) TO authenticated;
-- RPC: rpc_admin_get_delivery_staff_list
-- 管理端获取配送员分页列表
-- 支持按姓名或手机号搜索
CREATE OR REPLACE FUNCTION public.rpc_admin_get_delivery_staff_list(
p_search TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT NULL,
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_offset INTEGER := (p_page - 1) * p_page_size;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_delivery_staff
WHERE (p_status IS NULL OR status = p_status)
AND (p_search IS NULL OR p_search = '' OR nickname ILIKE '%' || p_search || '%' OR phone ILIKE '%' || p_search || '%');
-- 3. 获取明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, uid, nickname, avatar, phone, status, is_active,
created_at, updated_at
FROM public.ml_delivery_staff
WHERE (p_status IS NULL OR status = p_status)
AND (p_search IS NULL OR p_search = '' OR nickname ILIKE '%' || p_search || '%' OR phone ILIKE '%' || p_search || '%')
ORDER BY created_at DESC
LIMIT p_page_size OFFSET v_offset
) t;
-- 4. 返回结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER) TO authenticated;
-- RPC: rpc_admin_get_delivery_staff_list
-- 管理端获取服务人员分页列表v2
DROP FUNCTION IF EXISTS public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER);
CREATE OR REPLACE FUNCTION public.rpc_admin_get_delivery_staff_list(
p_search TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT NULL,
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_offset INTEGER := (p_page - 1) * p_page_size;
v_total BIGINT;
v_items JSONB;
BEGIN
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
SELECT COUNT(*) INTO v_total
FROM public.ml_delivery_staff s
WHERE s.deleted_at IS NULL
AND (p_status IS NULL OR s.status = p_status)
AND (
p_search IS NULL OR p_search = ''
OR s.nickname ILIKE '%' || p_search || '%'
OR s.phone ILIKE '%' || p_search || '%'
OR s.staff_no ILIKE '%' || p_search || '%'
);
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
s.id,
s.uid,
s.station_id,
st.name AS station_name,
s.staff_no,
s.nickname,
s.avatar,
s.phone,
s.status,
s.is_active,
s.online_status,
s.certificate_status,
s.certificate_expire_at,
s.service_area,
s.skills,
s.created_at,
s.updated_at
FROM public.ml_delivery_staff s
LEFT JOIN public.ml_delivery_stations st ON st.id = s.station_id AND st.deleted_at IS NULL
WHERE s.deleted_at IS NULL
AND (p_status IS NULL OR s.status = p_status)
AND (
p_search IS NULL OR p_search = ''
OR s.nickname ILIKE '%' || p_search || '%'
OR s.phone ILIKE '%' || p_search || '%'
OR s.staff_no ILIKE '%' || p_search || '%'
)
ORDER BY s.created_at DESC
LIMIT p_page_size OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_delivery_staff_list(TEXT, SMALLINT, INTEGER, INTEGER) TO authenticated;-- RPC: rpc_admin_get_delivery_station_list
-- 管理端获取提货点/核销点分页列表
-- 支持按名称、地址或手机号搜索
CREATE OR REPLACE FUNCTION public.rpc_admin_get_delivery_station_list(
p_search TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT NULL,
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 20
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_offset INTEGER := (p_page - 1) * p_page_size;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_delivery_stations
WHERE (p_status IS NULL OR status = p_status)
AND (p_search IS NULL OR p_search = ''
OR name ILIKE '%' || p_search || '%'
OR address ILIKE '%' || p_search || '%'
OR phone ILIKE '%' || p_search || '%');
-- 3. 获取明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, name, phone, address, image,
lng, lat, status, sort_order, business_hours,
created_at, updated_at
FROM public.ml_delivery_stations
WHERE (p_status IS NULL OR status = p_status)
AND (p_search IS NULL OR p_search = ''
OR name ILIKE '%' || p_search || '%'
OR address ILIKE '%' || p_search || '%'
OR phone ILIKE '%' || p_search || '%')
ORDER BY sort_order ASC, created_at DESC
LIMIT p_page_size OFFSET v_offset
) t;
-- 4. 返回结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_delivery_station_list(TEXT, SMALLINT, INTEGER, INTEGER) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_delivery_station_list(TEXT, SMALLINT, INTEGER, INTEGER) TO authenticated;
-- RPC: rpc_admin_save_delivery_staff
-- 管理端新增或更新配送员信息
CREATE OR REPLACE FUNCTION public.rpc_admin_save_delivery_staff(
p_id UUID DEFAULT NULL,
p_nickname TEXT DEFAULT NULL,
p_avatar TEXT DEFAULT NULL,
p_phone TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT 1
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 参数校验
IF p_nickname IS NULL OR p_phone IS NULL THEN
RAISE EXCEPTION 'Missing required fields: nickname or phone';
END IF;
-- 3. 新增或更新
IF p_id IS NULL THEN
INSERT INTO public.ml_delivery_staff (
nickname, avatar, phone, status
) VALUES (
p_nickname, p_avatar, p_phone, p_status
) RETURNING id INTO v_id;
ELSE
UPDATE public.ml_delivery_staff
SET
nickname = COALESCE(p_nickname, nickname),
avatar = COALESCE(p_avatar, avatar),
phone = COALESCE(p_phone, phone),
status = COALESCE(p_status, status),
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Delivery staff not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_delivery_staff(UUID, TEXT, TEXT, TEXT, SMALLINT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_delivery_staff(UUID, TEXT, TEXT, TEXT, SMALLINT) TO authenticated;
-- RPC: rpc_admin_save_delivery_staff
-- 管理端新增或更新服务人员信息v2
DROP FUNCTION IF EXISTS public.rpc_admin_save_delivery_staff(UUID, TEXT, TEXT, TEXT, SMALLINT);
CREATE OR REPLACE FUNCTION public.rpc_admin_save_delivery_staff(
p_id UUID DEFAULT NULL,
p_uid UUID DEFAULT NULL,
p_station_id UUID DEFAULT NULL,
p_staff_no TEXT DEFAULT NULL,
p_nickname TEXT DEFAULT NULL,
p_avatar TEXT DEFAULT NULL,
p_phone TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT 1,
p_online_status TEXT DEFAULT 'resting',
p_certificate_status TEXT DEFAULT 'pending',
p_certificate_expire_at DATE DEFAULT NULL,
p_service_area TEXT DEFAULT '',
p_skills JSONB DEFAULT '[]'::jsonb
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_id UUID;
BEGIN
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
IF p_nickname IS NULL OR p_phone IS NULL THEN
RAISE EXCEPTION 'Missing required fields: nickname or phone';
END IF;
IF p_online_status NOT IN ('online', 'resting', 'busy') THEN
RAISE EXCEPTION 'Invalid online_status';
END IF;
IF p_certificate_status NOT IN ('valid', 'expired', 'pending') THEN
RAISE EXCEPTION 'Invalid certificate_status';
END IF;
IF p_station_id IS NOT NULL AND NOT EXISTS (
SELECT 1
FROM public.ml_delivery_stations s
WHERE s.id = p_station_id AND s.deleted_at IS NULL
) THEN
RAISE EXCEPTION 'Delivery station not found';
END IF;
IF p_id IS NULL THEN
INSERT INTO public.ml_delivery_staff (
uid, station_id, staff_no, nickname, avatar, phone, status,
online_status, certificate_status, certificate_expire_at,
service_area, skills
) VALUES (
p_uid, p_station_id, NULLIF(p_staff_no, ''), p_nickname, p_avatar, p_phone, p_status,
p_online_status, p_certificate_status, p_certificate_expire_at,
COALESCE(p_service_area, ''), COALESCE(p_skills, '[]'::jsonb)
) RETURNING id INTO v_id;
ELSE
UPDATE public.ml_delivery_staff
SET
uid = COALESCE(p_uid, uid),
station_id = p_station_id,
staff_no = CASE WHEN p_staff_no IS NULL OR p_staff_no = '' THEN NULL ELSE p_staff_no END,
nickname = COALESCE(p_nickname, nickname),
avatar = COALESCE(p_avatar, avatar),
phone = COALESCE(p_phone, phone),
status = COALESCE(p_status, status),
online_status = COALESCE(p_online_status, online_status),
certificate_status = COALESCE(p_certificate_status, certificate_status),
certificate_expire_at = p_certificate_expire_at,
service_area = COALESCE(p_service_area, service_area),
skills = COALESCE(p_skills, skills),
updated_at = now()
WHERE id = p_id AND deleted_at IS NULL
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Delivery staff not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_save_delivery_staff(UUID, UUID, UUID, TEXT, TEXT, TEXT, TEXT, SMALLINT, TEXT, TEXT, DATE, TEXT, JSONB) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_delivery_staff(UUID, UUID, UUID, TEXT, TEXT, TEXT, TEXT, SMALLINT, TEXT, TEXT, DATE, TEXT, JSONB) TO authenticated;-- RPC: rpc_admin_save_delivery_station
-- 管理端新增或更新提货点/核销点信息
CREATE OR REPLACE FUNCTION public.rpc_admin_save_delivery_station(
p_id UUID DEFAULT NULL,
p_name TEXT DEFAULT NULL,
p_phone TEXT DEFAULT NULL,
p_address TEXT DEFAULT NULL,
p_image TEXT DEFAULT NULL,
p_lng NUMERIC DEFAULT NULL,
p_lat NUMERIC DEFAULT NULL,
p_status SMALLINT DEFAULT 1,
p_sort_order INTEGER DEFAULT 0,
p_business_hours JSONB DEFAULT NULL
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 参数校验
IF p_name IS NULL OR p_phone IS NULL OR p_address IS NULL THEN
RAISE EXCEPTION 'Missing required fields: name, phone or address';
END IF;
-- 3. 新增或更新
IF p_id IS NULL THEN
INSERT INTO public.ml_delivery_stations (
name, phone, address, image, lng, lat, status, sort_order, business_hours
) VALUES (
p_name, p_phone, p_address, p_image, p_lng, p_lat, p_status, p_sort_order, p_business_hours
) RETURNING id INTO v_id;
ELSE
UPDATE public.ml_delivery_stations
SET
name = COALESCE(p_name, name),
phone = COALESCE(p_phone, phone),
address = COALESCE(p_address, address),
image = COALESCE(p_image, image),
lng = COALESCE(p_lng, lng),
lat = COALESCE(p_lat, lat),
status = COALESCE(p_status, status),
sort_order = COALESCE(p_sort_order, sort_order),
business_hours = COALESCE(p_business_hours, business_hours),
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Station not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_save_delivery_station(UUID, TEXT, TEXT, TEXT, TEXT, NUMERIC, NUMERIC, SMALLINT, INTEGER, JSONB) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_delivery_station(UUID, TEXT, TEXT, TEXT, TEXT, NUMERIC, NUMERIC, SMALLINT, INTEGER, JSONB) TO authenticated;
-- =====================================================================================
-- RPC: rpc_delivery_* homecare actions
-- Version: v1
-- Purpose: 为 delivery 页面当前 api/delivery.uts 已固定的 rpc_delivery_* 契约补齐 SQL 端实现。
-- Notes:
-- 1. 优先兼容 ec/hc 新链,检测不到表或执行失败时回退 hss 旧链。
-- 2. 返回 JSON 结构直接对齐 delivery/types/delivery.uts。
-- 3. message_list 与 record_list 采用最小可用实现,避免前端继续落入 fallback。
-- =====================================================================================
-- 迁移正文见同批文件mall_sql/migrations/20260526_delivery_homecare_rpc_v1.sql-- =====================================================================================
-- RPC: rpc_homecare_dispatch_candidate
-- Version: v1
-- Purpose: 为居家上门服务返回单个可派单候选人,替代公开可派单人员 RLS。
-- Security: SECURITY DEFINER + 固定 search_path
-- Depends: public.ak_users, public.ml_delivery_staff
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_homecare_dispatch_candidate(
p_service_code TEXT DEFAULT NULL,
p_station_id UUID DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_candidate JSONB;
BEGIN
IF auth.uid() IS NULL OR NOT EXISTS (
SELECT 1
FROM public.ak_users u
WHERE u.auth_id = auth.uid()
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
SELECT jsonb_build_object(
'id', s.id,
'uid', s.uid,
'station_id', s.station_id,
'status', s.status,
'online_status', s.online_status,
'updated_at', s.updated_at,
'created_at', s.created_at
)
INTO v_candidate
FROM public.ml_delivery_staff s
WHERE s.deleted_at IS NULL
AND s.status = 1
AND COALESCE(s.is_active, TRUE) = TRUE
AND s.online_status = 'online'
AND s.uid IS NOT NULL
AND (p_station_id IS NULL OR s.station_id = p_station_id)
ORDER BY COALESCE(s.updated_at, s.created_at) DESC, s.created_at DESC
LIMIT 1;
RETURN v_candidate;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_homecare_dispatch_candidate(TEXT, UUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_homecare_dispatch_candidate(TEXT, UUID) TO authenticated;-- RPC: rpc_admin_delete_agent
-- 管理端删除代理商(支持级联软删除代理申请记录)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_agent(
p_uid uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_ok boolean;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('distribution:agent:delete') THEN
RAISE EXCEPTION 'Permission denied: distribution:agent:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 级联软删除:该代理的所有申请记录
UPDATE public.ak_distribution_agent_applications
SET deleted_at = now(),
deleted_by = v_user_id
WHERE user_id = p_uid AND deleted_at IS NULL;
-- 4. 软删除代理商记录
UPDATE public.ak_distribution_agents
SET deleted_at = now(),
deleted_by = v_user_id
WHERE uid = p_uid AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_delete_agent(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_agent(uuid) TO authenticated;
-- RPC: rpc_admin_delete_division
-- 管理端删除事业部(支持级联软删除关联代理)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_division(
p_uid uuid
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_ok boolean;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('distribution:division:delete') THEN
RAISE EXCEPTION 'Permission denied: distribution:division:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 级联软删除:该事业部下的所有代理商
UPDATE public.ak_distribution_agents
SET deleted_at = now(),
deleted_by = v_user_id
WHERE division_uid = p_uid AND deleted_at IS NULL;
-- 4. 级联软删除:该事业部的所有申请记录
UPDATE public.ak_distribution_division_applications
SET deleted_at = now(),
deleted_by = v_user_id
WHERE user_id = p_uid AND deleted_at IS NULL;
-- 5. 软删除事业部本身
UPDATE public.ak_distribution_divisions
SET deleted_at = now(),
deleted_by = v_user_id
WHERE uid = p_uid AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_delete_division(uuid) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_delete_division(uuid) TO authenticated;
-- RPC: rpc_admin_get_agent_apply_list
-- 管理端获取代理商申请列表
-- 支持按状态过滤all, pending, approved, rejected
CREATE OR REPLACE FUNCTION public.rpc_admin_get_agent_apply_list(
p_status text DEFAULT 'all',
p_search text DEFAULT NULL,
p_page integer DEFAULT 1,
p_page_size integer DEFAULT 20
)
RETURNS TABLE (
id uuid,
uid uuid,
name text,
phone text,
dept_uid uuid,
dept_name text,
proof_images jsonb,
status text,
refusal_reason text,
time timestamptz,
invite_code text
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_page integer := GREATEST(1, COALESCE(p_page, 1));
v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
v_offset integer := (v_page - 1) * v_page_size;
BEGIN
-- 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
RETURN QUERY
SELECT
a.id,
a.uid,
a.agent_name AS name,
a.agent_phone AS phone,
a.division_uid AS dept_uid,
d.name AS dept_name,
a.proof_images,
a.status,
a.refusal_reason,
a.created_at AS time,
d.invite_code
FROM public.ak_distribution_agent_applications a
JOIN public.ak_distribution_divisions d ON d.uid = a.division_uid
WHERE (p_status = 'all' OR a.status = p_status)
AND (
p_search IS NULL OR p_search = ''
OR a.agent_name ILIKE ('%' || p_search || '%')
OR a.uid::text ILIKE ('%' || p_search || '%')
)
ORDER BY a.created_at DESC
LIMIT v_page_size OFFSET v_offset;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_get_agent_apply_list(text, text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_agent_apply_list(text, text, integer, integer) TO authenticated;
-- RPC: rpc_admin_get_agent_list
-- 管理端获取代理商列表
-- 支持搜索代理商名称或负责人UID并关联显示所属事业部信息
CREATE OR REPLACE FUNCTION public.rpc_admin_get_agent_list(
p_search text DEFAULT NULL,
p_page integer DEFAULT 1,
p_page_size integer DEFAULT 20
)
RETURNS TABLE (
uid uuid,
name text,
division_uid uuid,
division_name text,
commission_ratio numeric,
is_enabled boolean,
end_time timestamptz,
created_at timestamptz,
"staffCount" bigint
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_page integer := GREATEST(1, COALESCE(p_page, 1));
v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
v_offset integer := (v_page - 1) * v_page_size;
BEGIN
-- 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
RETURN QUERY
SELECT
a.uid,
a.name,
a.division_uid,
d.name AS division_name,
a.commission_ratio,
a.is_enabled,
a.end_time,
a.created_at,
(SELECT COUNT(*) FROM public.ak_promoter_relations r WHERE r.inviter_uid = a.uid)::bigint AS "staffCount"
FROM public.ak_distribution_agents a
JOIN public.ak_distribution_divisions d ON d.uid = a.division_uid
WHERE (
p_search IS NULL OR p_search = ''
OR a.name ILIKE ('%' || p_search || '%')
OR a.uid::text ILIKE ('%' || p_search || '%')
)
ORDER BY a.created_at DESC
LIMIT v_page_size OFFSET v_offset;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_get_agent_list(text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_agent_list(text, integer, integer) TO authenticated;
-- RPC: rpc_admin_get_division_list
-- 管理端获取事业部列表
-- 支持搜索事业部名称或负责人UID
CREATE OR REPLACE FUNCTION public.rpc_admin_get_division_list(
p_search text DEFAULT NULL,
p_page integer DEFAULT 1,
p_page_size integer DEFAULT 20
)
RETURNS TABLE (
uid uuid,
name text,
invite_code text,
commission_ratio numeric,
is_enabled boolean,
end_time timestamptz,
created_at timestamptz,
"agentCount" bigint
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_page integer := GREATEST(1, COALESCE(p_page, 1));
v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
v_offset integer := (v_page - 1) * v_page_size;
BEGIN
-- 仅管理员或分析员可调用
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
RETURN QUERY
SELECT
d.uid,
d.name,
d.invite_code,
d.commission_ratio,
d.is_enabled,
d.end_time,
d.created_at,
(SELECT COUNT(*) FROM public.ak_distribution_agents a WHERE a.division_uid = d.uid)::bigint AS "agentCount"
FROM public.ak_distribution_divisions d
WHERE (
p_search IS NULL OR p_search = ''
OR d.name ILIKE ('%' || p_search || '%')
OR d.uid::text ILIKE ('%' || p_search || '%')
)
ORDER BY d.created_at DESC
LIMIT v_page_size OFFSET v_offset;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_get_division_list(text, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_division_list(text, integer, integer) TO authenticated;
-- RPC: rpc_admin_get_promoter_list
-- 管理端推广员列表聚合统计
-- 口径:集合=B上级+下级都算)=> 关系表中出现过的 uid/inviter_uid 都算推广员候选
-- 统计:
-- - 推广用户数量:以该用户作为 inviter_uid 的下级人数
-- - 推广订单数量/金额:其下级用户在 ml_orders 中已完成(order_status=4)的订单数与 paid_amount 汇总
-- - 佣金:从 ak_commission_logs 聚合
CREATE OR REPLACE FUNCTION public.rpc_admin_get_promoter_list(
p_search text DEFAULT NULL,
p_page integer DEFAULT 1,
p_page_size integer DEFAULT 20,
p_start_time timestamptz DEFAULT NULL,
p_end_time timestamptz DEFAULT NULL
)
RETURNS TABLE (
id uuid,
nickname text,
name text,
phone text,
avatar_url text,
level text,
"userCount" bigint,
"orderCount" bigint,
"orderAmount" numeric,
"commissionTotal" numeric,
"withdrawnAmount" numeric,
"withdrawCount" bigint,
"unwithdrawnAmount" numeric
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_page integer := GREATEST(1, COALESCE(p_page, 1));
v_page_size integer := LEAST(200, GREATEST(1, COALESCE(p_page_size, 20)));
v_offset integer := (v_page - 1) * v_page_size;
BEGIN
-- 仅管理员可调用
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
RETURN QUERY
WITH promoters AS (
SELECT DISTINCT x.uid
FROM (
SELECT r.uid FROM public.ak_promoter_relations r
UNION
SELECT r.inviter_uid FROM public.ak_promoter_relations r
) x
),
base AS (
SELECT
u.id,
u.username AS nickname,
u.real_name AS name,
u.phone,
u.avatar_url,
u.role AS level
FROM promoters p
JOIN public.ak_users u ON u.id = p.uid
WHERE (
p_search IS NULL OR p_search = ''
OR u.username ILIKE ('%' || p_search || '%')
OR COALESCE(u.real_name, '') ILIKE ('%' || p_search || '%')
OR COALESCE(u.phone, '') ILIKE ('%' || p_search || '%')
OR u.id::text ILIKE ('%' || p_search || '%')
)
),
downline AS (
SELECT inviter_uid, uid
FROM public.ak_promoter_relations
),
user_stats AS (
SELECT
d.inviter_uid AS id,
COUNT(*)::bigint AS "userCount"
FROM downline d
GROUP BY d.inviter_uid
),
order_stats AS (
SELECT
d.inviter_uid AS id,
COUNT(o.id)::bigint AS "orderCount",
COALESCE(SUM(o.paid_amount), 0)::numeric AS "orderAmount"
FROM downline d
JOIN public.ml_orders o ON o.user_id = d.uid
WHERE o.order_status = 4
AND (p_start_time IS NULL OR o.completed_at >= p_start_time)
AND (p_end_time IS NULL OR o.completed_at <= p_end_time)
GROUP BY d.inviter_uid
),
commission_stats AS (
SELECT
c.uid AS id,
COALESCE(SUM(c.amount), 0)::numeric AS "commissionTotal",
COALESCE(SUM(CASE WHEN c.status = 'withdrawn' THEN c.amount ELSE 0 END), 0)::numeric AS "withdrawnAmount",
0::bigint AS "withdrawCount",
COALESCE(SUM(CASE WHEN c.status IN ('frozen','available') THEN c.amount ELSE 0 END), 0)::numeric AS "unwithdrawnAmount"
FROM public.ak_commission_logs c
GROUP BY c.uid
)
SELECT
b.id,
b.nickname,
b.name,
b.phone,
b.avatar_url,
b.level,
COALESCE(us."userCount", 0) AS "userCount",
COALESCE(os."orderCount", 0) AS "orderCount",
COALESCE(os."orderAmount", 0) AS "orderAmount",
COALESCE(cs."commissionTotal", 0) AS "commissionTotal",
COALESCE(cs."withdrawnAmount", 0) AS "withdrawnAmount",
COALESCE(cs."withdrawCount", 0) AS "withdrawCount",
COALESCE(cs."unwithdrawnAmount", 0) AS "unwithdrawnAmount"
FROM base b
LEFT JOIN user_stats us ON us.id = b.id
LEFT JOIN order_stats os ON os.id = b.id
LEFT JOIN commission_stats cs ON cs.id = b.id
ORDER BY b.id
LIMIT v_page_size OFFSET v_offset;
END;
$$;
-- 授权:仅允许 authenticated 调用,函数内部再做 admin 校验
REVOKE ALL ON FUNCTION public.rpc_admin_get_promoter_list(text, integer, integer, timestamptz, timestamptz) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_promoter_list(text, integer, integer, timestamptz, timestamptz) TO authenticated;
-- RPC: rpc_admin_process_agent_apply
-- 管理端审核代理商申请
-- 若通过(approved),则同步在 ak_distribution_agents 中创建或更新记录
CREATE OR REPLACE FUNCTION public.rpc_admin_process_agent_apply(
p_id uuid,
p_status text, -- approved / rejected
p_refusal_reason text DEFAULT NULL
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_uid uuid;
v_division_uid uuid;
v_agent_name text;
BEGIN
-- 仅管理员可审核
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 1. 获取并锁定申请记录
SELECT uid, division_uid, agent_name
INTO v_uid, v_division_uid, v_agent_name
FROM public.ak_distribution_agent_applications
WHERE id = p_id;
IF v_uid IS NULL THEN
RAISE EXCEPTION 'application record not found';
END IF;
-- 2. 更新申请状态
UPDATE public.ak_distribution_agent_applications
SET
status = p_status,
refusal_reason = CASE WHEN p_status = 'rejected' THEN p_refusal_reason ELSE NULL END,
approved_at = now(),
approved_by = auth.uid(),
updated_at = now()
WHERE id = p_id;
-- 3. 如果通过,则同步到代理商正式表
IF p_status = 'approved' THEN
INSERT INTO public.ak_distribution_agents (
uid, division_uid, name, commission_ratio, is_enabled, updated_at, updated_by
)
VALUES (
v_uid, v_division_uid, v_agent_name, 0, true, now(), auth.uid()
)
ON CONFLICT (uid) DO UPDATE
SET
division_uid = EXCLUDED.division_uid,
name = EXCLUDED.name,
updated_at = now(),
updated_by = auth.uid();
END IF;
RETURN true;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_process_agent_apply(uuid, text, text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_process_agent_apply(uuid, text, text) TO authenticated;
-- RPC: rpc_admin_save_agent
-- 管理端新增或更新代理商
CREATE OR REPLACE FUNCTION public.rpc_admin_save_agent(
p_uid uuid,
p_division_uid uuid,
p_name text,
p_commission_ratio numeric,
p_is_enabled boolean DEFAULT true,
p_end_time timestamptz DEFAULT NULL
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- 仅管理员可操作
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
-- 确保事业部存在
IF NOT EXISTS (
SELECT 1 FROM public.ak_distribution_divisions WHERE uid = p_division_uid
) THEN
RAISE EXCEPTION 'parent division not found';
END IF;
INSERT INTO public.ak_distribution_agents (
uid, division_uid, name, commission_ratio, is_enabled, end_time, updated_at, updated_by
)
VALUES (
p_uid, p_division_uid, p_name, p_commission_ratio, p_is_enabled, p_end_time, now(), auth.uid()
)
ON CONFLICT (uid) DO UPDATE
SET
division_uid = EXCLUDED.division_uid,
name = EXCLUDED.name,
commission_ratio = EXCLUDED.commission_ratio,
is_enabled = EXCLUDED.is_enabled,
end_time = EXCLUDED.end_time,
updated_at = now(),
updated_by = auth.uid();
RETURN p_uid;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_save_agent(uuid, uuid, text, numeric, boolean, timestamptz) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_agent(uuid, uuid, text, numeric, boolean, timestamptz) TO authenticated;
-- RPC: rpc_admin_save_division
-- 管理端新增或更新事业部
CREATE OR REPLACE FUNCTION public.rpc_admin_save_division(
p_uid uuid,
p_name text,
p_invite_code text,
p_commission_ratio numeric,
p_is_enabled boolean DEFAULT true,
p_end_time timestamptz DEFAULT NULL
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
-- 仅管理员可操作
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
END IF;
INSERT INTO public.ak_distribution_divisions (
uid, name, invite_code, commission_ratio, is_enabled, end_time, updated_at, updated_by
)
VALUES (
p_uid, p_name, p_invite_code, p_commission_ratio, p_is_enabled, p_end_time, now(), auth.uid()
)
ON CONFLICT (uid) DO UPDATE
SET
name = EXCLUDED.name,
invite_code = EXCLUDED.invite_code,
commission_ratio = EXCLUDED.commission_ratio,
is_enabled = EXCLUDED.is_enabled,
end_time = EXCLUDED.end_time,
updated_at = now(),
updated_by = auth.uid();
RETURN p_uid;
END;
$$;
REVOKE ALL ON FUNCTION public.rpc_admin_save_division(uuid, text, text, numeric, boolean, timestamptz) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_save_division(uuid, text, text, numeric, boolean, timestamptz) TO authenticated;
-- =====================================================================================
-- Admin 财务统计 - 余额收支分布统计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按业务子类型统计指定时间范围内的余额收入与支出分布
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_balance_distribution(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_total_income DECIMAL(12,2);
v_total_expense DECIMAL(12,2);
v_income_items JSONB;
v_expense_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 计算总收入与总支出
SELECT
COALESCE(SUM(number) FILTER (WHERE pm = 1), 0),
COALESCE(SUM(number) FILTER (WHERE pm = 0), 0)
INTO v_total_income, v_total_expense
FROM public.ml_user_bill
WHERE category = 'balance'
AND created_at >= p_start_time
AND created_at <= p_end_time
AND status = 1;
-- 3. 统计收入分布 (来源分析)
SELECT jsonb_agg(t) INTO v_income_items
FROM (
SELECT
type AS name,
SUM(number) AS value,
CASE WHEN v_total_income > 0 THEN ROUND(SUM(number) / v_total_income * 100, 2) ELSE 0 END AS percent
FROM public.ml_user_bill
WHERE category = 'balance' AND pm = 1 AND status = 1
AND created_at >= p_start_time AND created_at <= p_end_time
GROUP BY type
ORDER BY value DESC
) t;
-- 4. 统计支出分布 (消耗分析)
SELECT jsonb_agg(t) INTO v_expense_items
FROM (
SELECT
type AS name,
SUM(number) AS value,
CASE WHEN v_total_expense > 0 THEN ROUND(SUM(number) / v_total_expense * 100, 2) ELSE 0 END AS percent
FROM public.ml_user_bill
WHERE category = 'balance' AND pm = 0 AND status = 1
AND created_at >= p_start_time AND created_at <= p_end_time
GROUP BY type
ORDER BY value DESC
) t;
RETURN jsonb_build_object(
'income', COALESCE(v_income_items, '[]'::jsonb),
'expense', COALESCE(v_expense_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_balance_distribution IS '统计财务余额收支来源与消耗分布';
-- =====================================================================================
-- Admin 财务统计 - 余额核心指标 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:获取全站当前余额存量、累计增加总额及累计消耗总额
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_balance_stats()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_current_balance DECIMAL(12,2);
v_total_accumulation DECIMAL(12,2);
v_total_consumption DECIMAL(12,2);
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 统计当前全站用户余额总存量
SELECT COALESCE(SUM(now_money), 0) INTO v_current_balance FROM public.ak_users;
-- 3. 统计累计增加 (pm=1) 和 累计消耗 (pm=0)
-- 基于 ml_user_bill 表中 category='balance' 的记录
SELECT
COALESCE(SUM(number) FILTER (WHERE pm = 1), 0),
COALESCE(SUM(number) FILTER (WHERE pm = 0), 0)
INTO v_total_accumulation, v_total_consumption
FROM public.ml_user_bill
WHERE category = 'balance' AND status = 1;
RETURN jsonb_build_object(
'current_balance', v_current_balance,
'total_accumulation', v_total_accumulation,
'total_consumption', v_total_consumption
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_balance_stats IS '获取全站余额存量及累计收支汇总';
-- =====================================================================================
-- Admin 财务统计 - 余额收支趋势 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按日聚合指定时间范围内的余额积累 (pm=1) 与 余额消耗 (pm=0) 趋势
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_balance_trend(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 按日聚合统计
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
to_char(date_trunc('day', gs.day), 'YYYY-MM-DD') AS date_group,
COALESCE(SUM(number) FILTER (WHERE pm = 1 AND category = 'balance'), 0) AS accumulation,
COALESCE(SUM(number) FILTER (WHERE pm = 0 AND category = 'balance'), 0) AS consumption
FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
LEFT JOIN public.ml_user_bill b ON date_trunc('day', b.created_at) = gs.day AND b.status = 1
GROUP BY gs.day
ORDER BY gs.day ASC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_balance_trend IS '按日聚合财务余额收支趋势';
-- =====================================================================================
-- Admin 财务功能 - 提现申请列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_extract, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_extract_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_status SMALLINT DEFAULT NULL,
p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_extract e
LEFT JOIN public.ak_users u ON u.id = e.uid
WHERE (p_status IS NULL OR e.status = p_status)
AND (p_start_time IS NULL OR e.created_at >= p_start_time)
AND (p_end_time IS NULL OR e.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.real_name, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.bank_code, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.alipay_code, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.wechat_code, '') ILIKE '%' || p_search || '%'
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
e.id,
e.uid,
e.real_name,
e.extract_type,
e.bank_code,
e.bank_address,
e.alipay_code,
e.wechat_code,
e.extract_price,
e.service_fee,
e.balance,
e.status,
e.refusal_reason,
e.admin_id,
e.payment_time,
e.created_at,
e.updated_at,
u.username as user_name,
u.email as user_email
FROM public.ml_extract e
LEFT JOIN public.ak_users u ON u.id = e.uid
WHERE (p_status IS NULL OR e.status = p_status)
AND (p_start_time IS NULL OR e.created_at >= p_start_time)
AND (p_end_time IS NULL OR e.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.real_name, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.bank_code, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.alipay_code, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.wechat_code, '') ILIKE '%' || p_search || '%'
))
ORDER BY e.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
-- 4. 返回结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_extract_list IS '管理员提现申请列表分页查询';
-- =====================================================================================
-- Admin 财务功能 - 提现审核 RPC (口径 2)
-- 位置docs/sql/30_rpc/finance/
-- 版本v1
-- 描述:提现审核通过时才扣除佣金并生成流水。
-- 安全策略SECURITY DEFINER, 入口鉴权, 固定 search_path
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_extract_review(
p_extract_id UUID,
p_status SMALLINT, -- 1: 通过, -1: 驳回
p_refusal_reason TEXT DEFAULT NULL
)
RETURNS VOID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_extract RECORD;
v_user RECORD;
BEGIN
-- 1. 鉴权:仅 admin 角色可执行
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 锁定并获取提现记录
SELECT * INTO v_extract FROM public.ml_extract WHERE id = p_extract_id FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'Extract record not found'; END IF;
IF v_extract.status != 0 THEN RAISE EXCEPTION 'Record already processed'; END IF;
-- 3. 业务处理
IF p_status = 1 THEN
-- 审核通过:锁定并校验用户资金
SELECT * INTO v_user FROM public.ak_users WHERE id = v_extract.uid FOR UPDATE;
IF v_user.brokerage_price < v_extract.extract_price THEN
RAISE EXCEPTION 'Insufficient brokerage balance';
END IF;
-- 扣除佣金
UPDATE public.ak_users
SET brokerage_price = brokerage_price - v_extract.extract_price
WHERE id = v_extract.uid;
-- 写入资金流水
INSERT INTO public.ml_user_bill (uid, link_id, pm, title, category, type, number, balance)
VALUES (
v_extract.uid,
p_extract_id::TEXT,
0, -- 支出
'佣金提现',
'brokerage',
'extract',
v_extract.extract_price,
v_user.brokerage_price - v_extract.extract_price
);
-- 更新提现记录
UPDATE public.ml_extract
SET status = 1, admin_id = auth.uid(), payment_time = now()
WHERE id = p_extract_id;
ELSIF p_status = -1 THEN
-- 审核驳回:仅更新状态
UPDATE public.ml_extract
SET status = -1, refusal_reason = p_refusal_reason, admin_id = auth.uid()
WHERE id = p_extract_id;
ELSE
RAISE EXCEPTION 'Invalid status';
END IF;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_extract_review IS '管理员审核提现申请(口径 2通过时扣款';
-- =====================================================================================
-- Admin 财务功能 - 账单汇总统计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按日/周/月维度聚合财务收支数据,支撑账单列表展示
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_finance_bill_summary(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE,
p_interval TEXT DEFAULT 'day' -- day, week, month
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 聚合统计
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
to_char(date_trunc(p_interval, created_at),
CASE
WHEN p_interval = 'day' THEN 'YYYY-MM-DD'
WHEN p_interval = 'week' THEN 'IYYY-IW'
ELSE 'YYYY-MM'
END
) AS date_group,
SUM(number) FILTER (WHERE pm = 1) AS income,
SUM(number) FILTER (WHERE pm = 0) AS expense,
SUM(CASE WHEN pm = 1 THEN number ELSE -number END) AS net_entry
FROM public.ml_user_bill
WHERE created_at >= p_start_time
AND created_at <= p_end_time
AND status = 1
GROUP BY date_trunc(p_interval, created_at)
ORDER BY date_trunc(p_interval, created_at) DESC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_finance_bill_summary IS '按周期聚合财务收支账单';
-- =====================================================================================
-- Admin 财务功能 - 财务概况统计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:获取指定时间段内的财务核心 KPI营业额、充值汇总、提现汇总、资金存量
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_finance_overview(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_recharge_amount DECIMAL(12,2);
v_recharge_count BIGINT;
v_extract_amount DECIMAL(12,2);
v_extract_count BIGINT;
v_total_user_balance DECIMAL(12,2);
v_total_user_brokerage DECIMAL(12,2);
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 统计充值 (仅统计已支付)
SELECT
COALESCE(SUM(price + give_price), 0),
COUNT(*)
INTO v_recharge_amount, v_recharge_count
FROM public.ml_user_recharge
WHERE paid = 1
AND created_at >= p_start_time
AND created_at <= p_end_time;
-- 3. 统计提现 (仅统计已通过)
SELECT
COALESCE(SUM(extract_price), 0),
COUNT(*)
INTO v_extract_amount, v_extract_count
FROM public.ml_extract
WHERE status = 1
AND created_at >= p_start_time
AND created_at <= p_end_time;
-- 4. 统计全站资金存量 (实时快照)
SELECT
COALESCE(SUM(now_money), 0),
COALESCE(SUM(brokerage_price), 0)
INTO v_total_user_balance, v_total_user_brokerage
FROM public.ak_users;
RETURN jsonb_build_object(
'recharge_amount', v_recharge_amount,
'recharge_count', v_recharge_count,
'extract_amount', v_extract_amount,
'extract_count', v_extract_count,
'total_user_balance', v_total_user_balance,
'total_user_brokerage', v_total_user_brokerage
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_finance_overview IS '财务核心 KPI 概况统计';
-- =====================================================================================
-- RPC: rpc_admin_invoice_list
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取发票申请列表,支持搜索、状态筛选及时间过滤
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_invoice_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_status SMALLINT DEFAULT NULL,
p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_invoices i
LEFT JOIN public.ak_users u ON u.id = i.uid
WHERE (p_status IS NULL OR i.status = p_status)
AND (p_start_time IS NULL OR i.created_at >= p_start_time)
AND (p_end_time IS NULL OR i.created_at <= p_end_time)
AND (p_search IS NULL OR (
i.order_no ILIKE '%' || p_search || '%' OR
i.header_name ILIKE '%' || p_search || '%' OR
u.username ILIKE '%' || p_search || '%'
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
i.id,
i.uid,
i.order_no,
i.order_amount,
i.invoice_type,
i.header_type,
i.header_name,
i.tax_id,
i.email,
i.remark,
i.status,
i.refusal_reason,
i.invoice_url,
i.created_at,
i.updated_at,
u.username as user_name,
u.email as user_email
FROM public.ml_invoices i
LEFT JOIN public.ak_users u ON u.id = i.uid
WHERE (p_status IS NULL OR i.status = p_status)
AND (p_start_time IS NULL OR i.created_at >= p_start_time)
AND (p_end_time IS NULL OR i.created_at <= p_end_time)
AND (p_search IS NULL OR (
i.order_no ILIKE '%' || p_search || '%' OR
i.header_name ILIKE '%' || p_search || '%' OR
u.username ILIKE '%' || p_search || '%'
))
ORDER BY i.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_invoice_list IS '管理员分页查询发票申请列表';
-- =====================================================================================
-- RPC: rpc_admin_invoice_process
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端处理发票申请(开票或驳回)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_invoice_process(
p_id UUID,
p_status SMALLINT, -- 1: 已开票, -1: 已拒绝
p_invoice_url TEXT DEFAULT NULL,
p_refusal_reason TEXT DEFAULT NULL
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 更新状态
UPDATE public.ml_invoices
SET
status = p_status,
invoice_url = CASE WHEN p_status = 1 THEN p_invoice_url ELSE invoice_url END,
refusal_reason = CASE WHEN p_status = -1 THEN p_refusal_reason ELSE refusal_reason END,
updated_at = now()
WHERE id = p_id;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_invoice_process IS '管理员处理发票开票申请';
-- =====================================================================================
-- Admin 财务功能 - 充值补单/审计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 版本v1
-- 描述:由管理员发起的人工充值补单或离线支付审计确认。
-- 安全策略SECURITY DEFINER, 入口鉴权, 固定 search_path
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_recharge_audit(
p_recharge_id UUID,
p_mark TEXT DEFAULT '管理员人工审计/补单'
)
RETURNS VOID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_recharge RECORD;
v_user RECORD;
BEGIN
-- 1. 鉴权:仅 admin 角色可执行
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 锁定并获取充值记录
SELECT * INTO v_recharge FROM public.ml_user_recharge WHERE id = p_recharge_id FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'Recharge record not found'; END IF;
IF v_recharge.paid = 1 THEN RAISE EXCEPTION 'Recharge already paid'; END IF;
-- 3. 锁定并更新用户余额
SELECT * INTO v_user FROM public.ak_users WHERE id = v_recharge.uid FOR UPDATE;
UPDATE public.ak_users
SET now_money = now_money + v_recharge.price + v_recharge.give_price
WHERE id = v_recharge.uid;
-- 4. 写入资金流水
INSERT INTO public.ml_user_bill (uid, link_id, pm, title, category, type, number, balance, mark)
VALUES (
v_recharge.uid,
v_recharge.order_no,
1, -- 收入
'用户充值',
'now_money',
'recharge',
v_recharge.price + v_recharge.give_price,
v_user.now_money + v_recharge.price + v_recharge.give_price,
p_mark
);
-- 5. 更新充值记录状态
UPDATE public.ml_user_recharge
SET paid = 1, pay_time = now()
WHERE id = p_recharge_id;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_recharge_audit IS '管理员人工审计/补单(更新用户余额并生成流水)';
-- =====================================================================================
-- Admin 财务功能 - 充值记录列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_user_recharge, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_recharge_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_paid SMALLINT DEFAULT NULL,
p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_user_recharge r
LEFT JOIN public.ak_users u ON u.id = r.uid
WHERE (p_paid IS NULL OR r.paid = p_paid)
AND (p_start_time IS NULL OR r.created_at >= p_start_time)
AND (p_end_time IS NULL OR r.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(r.order_no, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%'
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
r.id,
r.uid,
r.order_no,
r.recharge_type,
r.price,
r.give_price,
r.paid,
r.pay_time,
r.channel_trade_no,
r.status,
r.created_at,
r.updated_at,
u.username as user_name,
u.email as user_email
FROM public.ml_user_recharge r
LEFT JOIN public.ak_users u ON u.id = r.uid
WHERE (p_paid IS NULL OR r.paid = p_paid)
AND (p_start_time IS NULL OR r.created_at >= p_start_time)
AND (p_end_time IS NULL OR r.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(r.order_no, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%'
))
ORDER BY r.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
-- 4. 返回结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_recharge_list IS '管理员充值记录列表分页查询';
-- =====================================================================================
-- Admin 财务功能 - 资金流水列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_user_bill, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_bill_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_category VARCHAR DEFAULT NULL,
p_type VARCHAR DEFAULT NULL,
p_pm SMALLINT DEFAULT NULL,
p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_user_bill b
LEFT JOIN public.ak_users u ON u.id = b.uid
WHERE (p_category IS NULL OR b.category = p_category)
AND (p_type IS NULL OR b.type = p_type)
AND (p_pm IS NULL OR b.pm = p_pm)
AND (p_start_time IS NULL OR b.created_at >= p_start_time)
AND (p_end_time IS NULL OR b.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(b.title, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%'
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
b.id,
b.uid,
b.link_id,
b.pm,
b.title,
b.category,
b.type,
b.number,
b.balance,
b.mark,
b.status,
b.created_at,
b.updated_at,
u.username as user_name,
u.email as user_email
FROM public.ml_user_bill b
LEFT JOIN public.ak_users u ON u.id = b.uid
WHERE (p_category IS NULL OR b.category = p_category)
AND (p_type IS NULL OR b.type = p_type)
AND (p_pm IS NULL OR b.pm = p_pm)
AND (p_start_time IS NULL OR b.created_at >= p_start_time)
AND (p_end_time IS NULL OR b.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(b.title, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%'
))
ORDER BY b.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
-- 4. 返回结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_user_bill_list IS '管理员资金流水列表分页查询';
-- =====================================================================================
-- RPC: rpc_admin_kefu_account_delete
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除客服账号(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('kefu:account:delete') THEN
RAISE EXCEPTION 'Permission denied: kefu:account:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除:标记 deleted_at
UPDATE public.ml_kefu_accounts
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_account_delete IS '管理员删除客服账号';
-- =====================================================================================
-- RPC: rpc_admin_kefu_account_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取客服账号列表
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_search TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_kefu_accounts ka
JOIN public.ak_users u ON u.id = ka.user_id
WHERE (p_status IS NULL OR ka.status = p_status)
AND (p_search IS NULL OR ka.nickname ILIKE '%' || p_search || '%' OR u.username ILIKE '%' || p_search || '%');
-- 3. 获取数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
ka.id,
ka.user_id,
ka.nickname,
ka.avatar,
ka.status,
ka.is_online,
ka.created_at,
ka.updated_at,
u.username as user_account
FROM public.ml_kefu_accounts ka
JOIN public.ak_users u ON u.id = ka.user_id
WHERE (p_status IS NULL OR ka.status = p_status)
AND (p_search IS NULL OR ka.nickname ILIKE '%' || p_search || '%' OR u.username ILIKE '%' || p_search || '%')
ORDER BY ka.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_kefu_account_save
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:新增或更新客服账号
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_save(
p_id UUID DEFAULT NULL,
p_user_id UUID DEFAULT NULL,
p_nickname TEXT DEFAULT NULL,
p_avatar TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 新增
IF p_id IS NULL THEN
IF p_user_id IS NULL OR p_nickname IS NULL THEN
RAISE EXCEPTION 'Missing required fields';
END IF;
INSERT INTO public.ml_kefu_accounts (
user_id, nickname, avatar, status
) VALUES (
p_user_id, p_nickname, p_avatar, p_status
) RETURNING id INTO v_id;
ELSE
-- 3. 更新
UPDATE public.ml_kefu_accounts
SET
nickname = COALESCE(p_nickname, nickname),
avatar = COALESCE(p_avatar, avatar),
status = COALESCE(p_status, status),
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Account not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_kefu_account_set_status
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端切换客服账号启用/禁用状态
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_set_status(
p_id UUID,
p_status SMALLINT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 更新状态
UPDATE public.ml_kefu_accounts
SET status = p_status,
updated_at = now()
WHERE id = p_id;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_account_set_status IS '管理员设置客服账号状态';
-- =====================================================================================
-- RPC: rpc_admin_kefu_auto_reply_delete
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除客服自动回复配置(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('kefu:auto_reply:delete') THEN
RAISE EXCEPTION 'Permission denied: kefu:auto_reply:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除:标记 deleted_at
UPDATE public.ml_kefu_auto_replies
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_delete IS '管理员删除客服自动回复配置';
-- =====================================================================================
-- RPC: rpc_admin_kefu_auto_reply_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端获取客服自动回复配置列表
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_kefu_auto_replies
WHERE (p_search IS NULL OR keyword ILIKE '%' || p_search || '%' OR content ILIKE '%' || p_search || '%');
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, keyword, content, reply_type, status,
created_at, updated_at
FROM public.ml_kefu_auto_replies
WHERE (p_search IS NULL OR keyword ILIKE '%' || p_search || '%' OR content ILIKE '%' || p_search || '%')
ORDER BY created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_list IS '管理员分页查询客服自动回复列表';
-- =====================================================================================
-- RPC: rpc_admin_kefu_auto_reply_save
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新自动回复配置
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_save(
p_id UUID DEFAULT NULL,
p_keyword TEXT DEFAULT NULL,
p_content TEXT DEFAULT NULL,
p_reply_type TEXT DEFAULT 'text',
p_status SMALLINT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 新增
IF p_id IS NULL THEN
IF p_keyword IS NULL OR p_content IS NULL THEN
RAISE EXCEPTION 'Missing required fields: keyword or content';
END IF;
INSERT INTO public.ml_kefu_auto_replies (
keyword, content, reply_type, status
) VALUES (
p_keyword, p_content, p_reply_type, p_status
) RETURNING id INTO v_id;
ELSE
-- 3. 更新
UPDATE public.ml_kefu_auto_replies
SET
keyword = COALESCE(p_keyword, keyword),
content = COALESCE(p_content, content),
reply_type = COALESCE(p_reply_type, reply_type),
status = COALESCE(p_status, status),
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Auto reply record not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_save IS '管理员新增或更新自动回复配置';
-- =====================================================================================
-- RPC: rpc_admin_kefu_auto_reply_set_status
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端切换客服自动回复配置启用/禁用状态
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_set_status(
p_id UUID,
p_status SMALLINT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 更新状态
UPDATE public.ml_kefu_auto_replies
SET status = p_status,
updated_at = now()
WHERE id = p_id;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_set_status IS '管理员设置客服自动回复状态';
-- =====================================================================================
-- RPC: rpc_admin_kefu_feedback_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取用户留言反馈列表
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_feedback_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_search TEXT DEFAULT NULL,
p_status SMALLINT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_kefu_feedbacks f
LEFT JOIN public.ak_users u ON u.id = f.user_id
WHERE (p_status IS NULL OR f.status = p_status)
AND (p_search IS NULL OR f.nickname ILIKE '%' || p_search || '%' OR f.phone ILIKE '%' || p_search || '%' OR f.content ILIKE '%' || p_search || '%');
-- 3. 获取数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
f.id,
f.user_id,
f.nickname,
f.phone,
f.content,
f.status,
f.reply_content,
f.processed_at,
f.created_at,
f.updated_at,
u.username as user_account
FROM public.ml_kefu_feedbacks f
LEFT JOIN public.ak_users u ON u.id = f.user_id
WHERE (p_status IS NULL OR f.status = p_status)
AND (p_search IS NULL OR f.nickname ILIKE '%' || p_search || '%' OR f.phone ILIKE '%' || p_search || '%' OR f.content ILIKE '%' || p_search || '%')
ORDER BY f.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_feedback_list IS '管理员分页查询用户留言反馈列表';
-- =====================================================================================
-- RPC: rpc_admin_kefu_feedback_process
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端处理用户留言反馈(回复内容并更新状态)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_feedback_process(
p_id UUID,
p_reply_content TEXT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 更新状态
UPDATE public.ml_kefu_feedbacks
SET
status = 1, -- 已处理
reply_content = p_reply_content,
processed_at = now(),
updated_at = now()
WHERE id = p_id;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_feedback_process IS '管理员处理并回复用户留言反馈';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_category_delete
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除话术分类(支持级联软删除话术)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('kefu:word:category:delete') THEN
RAISE EXCEPTION 'Permission denied: kefu:word:category:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 级联软删除:先标记该分类下的话术为删除
UPDATE public.ml_kefu_words
SET deleted_at = now(),
deleted_by = v_user_id
WHERE category_id = p_id AND deleted_at IS NULL;
-- 4. 执行软删除分类本身:标记 deleted_at
UPDATE public.ml_kefu_word_categories
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_delete IS '管理员删除话术分类';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_category_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端获取话术分类列表
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_list()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取分类列表
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT id, name, sort, created_at, updated_at
FROM public.ml_kefu_word_categories
ORDER BY sort ASC, created_at DESC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_list IS '管理员获取话术分类列表';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_category_save
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新话术分类
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_save(
p_id UUID DEFAULT NULL,
p_name TEXT DEFAULT NULL,
p_sort INTEGER DEFAULT 0
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 新增
IF p_id IS NULL THEN
IF p_name IS NULL THEN
RAISE EXCEPTION 'Missing required fields: name';
END IF;
INSERT INTO public.ml_kefu_word_categories (
name, sort
) VALUES (
p_name, p_sort
) RETURNING id INTO v_id;
ELSE
-- 3. 更新
UPDATE public.ml_kefu_word_categories
SET
name = COALESCE(p_name, name),
sort = COALESCE(p_sort, sort),
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Category not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_save IS '管理员新增或更新话术分类';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_delete
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除快捷话术(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('kefu:word:delete') THEN
RAISE EXCEPTION 'Permission denied: kefu:word:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除:标记 deleted_at
UPDATE public.ml_kefu_words
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_word_delete IS '管理员删除快捷话术';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_list
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端获取指定分类下的快捷话术列表
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_list(
p_category_id UUID DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取话术列表
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
w.id,
w.category_id,
w.title,
w.content,
w.sort,
w.created_at,
w.updated_at,
c.name as category_name
FROM public.ml_kefu_words w
JOIN public.ml_kefu_word_categories c ON c.id = w.category_id
WHERE (p_category_id IS NULL OR w.category_id = p_category_id)
AND (p_search IS NULL OR w.title ILIKE '%' || p_search || '%' OR w.content ILIKE '%' || p_search || '%')
ORDER BY w.sort ASC, w.created_at DESC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_word_list IS '管理员获取快捷话术列表';
-- =====================================================================================
-- RPC: rpc_admin_kefu_word_save
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端新增或更新快捷话术
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_save(
p_id UUID DEFAULT NULL,
p_category_id UUID DEFAULT NULL,
p_title TEXT DEFAULT NULL,
p_content TEXT DEFAULT NULL,
p_sort INTEGER DEFAULT 0
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 新增
IF p_id IS NULL THEN
IF p_category_id IS NULL OR p_title IS NULL OR p_content IS NULL THEN
RAISE EXCEPTION 'Missing required fields';
END IF;
INSERT INTO public.ml_kefu_words (
category_id, title, content, sort
) VALUES (
p_category_id, p_title, p_content, p_sort
) RETURNING id INTO v_id;
ELSE
-- 3. 更新
UPDATE public.ml_kefu_words
SET
category_id = COALESCE(p_category_id, category_id),
title = COALESCE(p_title, title),
content = COALESCE(p_content, content),
sort = COALESCE(p_sort, sort),
updated_at = now()
WHERE id = p_id
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Word not found';
END IF;
END IF;
RETURN v_id;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_kefu_word_save IS '管理员新增或更新快捷话术';
-- RPC: rpc_admin_get_integral_stats
-- 位置docs/sql/30_rpc/marketing/rpc_admin_get_integral_stats_v1.sql
-- 说明:聚合统计积分概况(总额、趋势、分布)
CREATE OR REPLACE FUNCTION public.rpc_admin_get_integral_stats(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_total_stats RECORD;
v_trend_data JSONB;
v_source_dist JSONB;
v_consume_dist JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 计算核心指标 (所有时间)
SELECT
COALESCE(SUM(CASE WHEN pm = 1 THEN number ELSE -number END), 0) as current_total,
COALESCE(SUM(CASE WHEN pm = 1 THEN number ELSE 0 END), 0) as cumulative_income,
COALESCE(SUM(CASE WHEN pm = 0 THEN number ELSE 0 END), 0) as cumulative_expend
INTO v_total_stats
FROM public.ml_user_bill
WHERE category = 'integral' AND status = 1;
-- 3. 趋势数据 (按日聚合)
SELECT jsonb_agg(t) INTO v_trend_data
FROM (
SELECT
to_char(date_trunc('day', gs.day), 'MM-DD') AS date_group,
COALESCE((SELECT SUM(number) FROM public.ml_user_bill b
WHERE b.category = 'integral' AND b.pm = 1 AND b.status = 1
AND date_trunc('day', b.created_at) = gs.day), 0) as income,
COALESCE((SELECT SUM(number) FROM public.ml_user_bill b
WHERE b.category = 'integral' AND b.pm = 0 AND b.status = 1
AND date_trunc('day', b.created_at) = gs.day), 0) as expend
FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
ORDER BY gs.day ASC
) t;
-- 4. 来源分布 (按 type 分组)
SELECT jsonb_agg(t) INTO v_source_dist
FROM (
SELECT
type as label,
SUM(number) as value,
ROUND((SUM(number) * 100 / NULLIF(v_total_stats.cumulative_income, 0)), 2) as percent
FROM public.ml_user_bill
WHERE category = 'integral' AND pm = 1 AND status = 1
GROUP BY type
ORDER BY value DESC
) t;
-- 5. 消耗分布 (按 type 分组)
SELECT jsonb_agg(t) INTO v_consume_dist
FROM (
SELECT
type as label,
SUM(number) as value,
ROUND((SUM(number) * 100 / NULLIF(v_total_stats.cumulative_expend, 0)), 2) as percent
FROM public.ml_user_bill
WHERE category = 'integral' AND pm = 0 AND status = 1
GROUP BY type
ORDER BY value DESC
) t;
RETURN jsonb_build_object(
'totals', jsonb_build_object(
'current', v_total_stats.current_total,
'income', v_total_stats.cumulative_income,
'expend', v_total_stats.cumulative_expend
),
'trend', COALESCE(v_trend_data, '[]'::jsonb),
'sources', COALESCE(v_source_dist, '[]'::jsonb),
'consumes', COALESCE(v_consume_dist, '[]'::jsonb)
);
END;
$$;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_integral_stats(timestamptz, timestamptz) TO authenticated;
-- =====================================================================================
-- Admin 订单功能 - 收银台订单列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_cashier_order_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_search_order_no TEXT DEFAULT NULL,
p_search_username TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 总数:仅已支付订单
SELECT COUNT(*) INTO v_total
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
WHERE o.paid_at IS NOT NULL
AND (p_search_order_no IS NULL OR o.order_no ILIKE '%' || p_search_order_no || '%')
AND (p_search_username IS NULL OR u.username ILIKE '%' || p_search_username || '%');
-- 3. 明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
o.id,
o.order_no,
o.total_amount,
o.discount_amount,
o.paid_at,
u.username as customer_name,
u.phone as customer_phone
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
WHERE o.paid_at IS NOT NULL
AND (p_search_order_no IS NULL OR o.order_no ILIKE '%' || p_search_order_no || '%')
AND (p_search_username IS NULL OR u.username ILIKE '%' || p_search_username || '%')
ORDER BY o.paid_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;-- =====================================================================================
-- Admin 订单管理 - 主订单列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_order_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_order_status INTEGER DEFAULT NULL,
p_search TEXT DEFAULT NULL,
p_start_time TIMESTAMPTZ DEFAULT NULL,
p_end_time TIMESTAMPTZ DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查 (依赖 public.ak_users.role)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
WHERE (p_order_status IS NULL OR o.order_status = p_order_status)
AND (p_start_time IS NULL OR o.created_at >= p_start_time)
AND (p_end_time IS NULL OR o.created_at <= p_end_time)
AND (
p_search IS NULL
OR o.order_no ILIKE '%' || p_search || '%'
OR u.username ILIKE '%' || p_search || '%'
OR u.phone ILIKE '%' || p_search || '%'
);
-- 3. 获取明细列表
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
o.id,
o.order_no,
o.total_amount,
o.paid_amount,
o.discount_amount,
o.order_status,
o.payment_status,
o.shipping_status,
o.pay_type,
o.channel_type,
o.paid_at,
o.created_at,
u.username as buyer_name,
u.phone as buyer_phone,
(
SELECT jsonb_build_object(
'product_name', oi.product_name,
'image_url', oi.image_url,
'quantity', oi.quantity
)
FROM public.ml_order_items oi
WHERE oi.order_id = o.id
ORDER BY oi.created_at ASC
LIMIT 1
) as first_item_summary
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
WHERE (p_order_status IS NULL OR o.order_status = p_order_status)
AND (p_start_time IS NULL OR o.created_at >= p_start_time)
AND (p_end_time IS NULL OR o.created_at <= p_end_time)
AND (
p_search IS NULL
OR o.order_no ILIKE '%' || p_search || '%'
OR u.username ILIKE '%' || p_search || '%'
OR u.phone ILIKE '%' || p_search || '%'
)
ORDER BY o.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- =====================================================================================
-- Admin 订单统计 - 订单来源分布 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:统计订单来源分布。
-- 注意:当前 ml_orders DDL 未包含来源/渠道字段,本函数提供最小可用兜底:统一返回 "unknown" 汇总。
-- 若后续新增 channel/payment_method 等字段,可在此函数中替换为按渠道分组统计。
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_order_source_stats(
p_start_time TIMESTAMPTZ,
p_end_time TIMESTAMPTZ
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 按渠道类型聚合统计(排除已取消)
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
CASE o.channel_type
WHEN 1 THEN '公众号'
WHEN 2 THEN '小程序'
WHEN 3 THEN 'H5'
WHEN 4 THEN 'PC'
WHEN 5 THEN 'APP'
ELSE '其他'
END AS source,
COUNT(*) AS order_count,
COALESCE(SUM(o.total_amount), 0) AS total_amount
FROM public.ml_orders o
WHERE o.created_at >= p_start_time
AND o.created_at <= p_end_time
AND o.order_status != 5
GROUP BY o.channel_type
ORDER BY order_count DESC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- Admin 订单统计 - 核心 KPI 汇总 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:获取指定时间段内的订单量、销售额、退款数及退款金额
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_order_stats(
p_start_time TIMESTAMPTZ,
p_end_time TIMESTAMPTZ
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_order_count BIGINT;
v_total_amount DECIMAL(12,2);
v_refund_count BIGINT;
v_refund_amount DECIMAL(12,2);
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 统计订单汇总(不含已取消)
SELECT
COUNT(*),
COALESCE(SUM(total_amount), 0)
INTO v_order_count, v_total_amount
FROM public.ml_orders
WHERE created_at >= p_start_time
AND created_at <= p_end_time
AND order_status != 5; -- 5: 已取消
-- 3. 统计退款汇总
-- 注意:这里基于 ml_orders 的 payment_status 或 order_status 判断已退款
SELECT
COUNT(*),
COALESCE(SUM(discount_amount), 0) -- 暂时用这个,若有真实退款金额字段请替换
INTO v_refund_count, v_refund_amount
FROM public.ml_orders
WHERE created_at >= p_start_time
AND created_at <= p_end_time
AND order_status IN (6, 7); -- 6: 退款中, 7: 已退款
RETURN jsonb_build_object(
'order_count', v_order_count,
'total_amount', v_total_amount,
'refund_count', v_refund_count,
'refund_amount', v_refund_amount
);
END;
$$;
-- =====================================================================================
-- Admin 订单统计 - 趋势统计 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按天聚合指定时间范围内的订单量/销售额/退款量/退款金额
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_order_trend(
p_start_time TIMESTAMPTZ,
p_end_time TIMESTAMPTZ,
p_group_by TEXT DEFAULT 'day'
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 目前仅支持 day
IF p_group_by IS NULL OR p_group_by != 'day' THEN
RAISE EXCEPTION 'Unsupported group_by';
END IF;
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
to_char(date_trunc('day', o.created_at), 'YYYY-MM-DD') AS date_group,
COUNT(*) FILTER (WHERE o.order_status != 5) AS order_count,
COALESCE(SUM(o.total_amount) FILTER (WHERE o.order_status != 5), 0) AS total_amount,
COUNT(*) FILTER (WHERE o.order_status IN (6, 7)) AS refund_count,
COALESCE(SUM(o.discount_amount) FILTER (WHERE o.order_status IN (6, 7)), 0) AS refund_amount
FROM public.ml_orders o
WHERE o.created_at >= p_start_time
AND o.created_at <= p_end_time
GROUP BY date_trunc('day', o.created_at)
ORDER BY date_trunc('day', o.created_at) ASC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- Admin 订单统计 - 订单类型分布统计 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按订单类型(普通、收银、核销)统计指定时间段内的销售额及其占比
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_order_type_stats(
p_start_time TIMESTAMPTZ,
p_end_time TIMESTAMPTZ
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_total_amount DECIMAL(12,2);
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 计算总销售额(用于算占比)
SELECT COALESCE(SUM(total_amount), 0) INTO v_total_amount
FROM public.ml_orders
WHERE created_at >= p_start_time AND created_at <= p_end_time
AND order_status != 5; -- 排除已取消
-- 3. 按类型统计
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
CASE o.order_type
WHEN 1 THEN '普通订单'
WHEN 2 THEN '收银订单'
WHEN 3 THEN '核销订单'
ELSE '其他类型'
END AS name,
COALESCE(SUM(o.total_amount), 0) AS amount,
CASE
WHEN v_total_amount > 0 THEN ROUND((COALESCE(SUM(o.total_amount), 0) / v_total_amount * 100), 2)
ELSE 0
END AS rate
FROM public.ml_orders o
WHERE o.created_at >= p_start_time AND o.created_at <= p_end_time
AND o.order_status != 5
GROUP BY o.order_type
ORDER BY amount DESC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- Admin 订单功能 - 售后退款列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_refund_order_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_refund_status INTEGER DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_refund_orders ro
WHERE (p_refund_status IS NULL OR ro.refund_status = p_refund_status)
AND (p_search IS NULL OR (
ro.refund_no ILIKE '%' || p_search || '%' OR
EXISTS (
SELECT 1 FROM public.ml_orders o
WHERE o.id = ro.order_id AND o.order_no ILIKE '%' || p_search || '%'
)
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
ro.id,
ro.refund_no,
ro.refund_amount,
ro.refund_status,
ro.refund_reason,
ro.applied_at,
o.order_no,
o.order_status,
u.username as customer_name,
u.phone as customer_phone,
(
SELECT jsonb_build_object(
'product_name', oi.product_name,
'image_url', oi.image_url
)
FROM public.ml_order_items oi
WHERE oi.order_id = ro.order_id
LIMIT 1
) as product_summary
FROM public.ml_refund_orders ro
LEFT JOIN public.ml_orders o ON ro.order_id = o.id
LEFT JOIN public.ak_users u ON ro.user_id = u.id
WHERE (p_refund_status IS NULL OR ro.refund_status = p_refund_status)
AND (p_search IS NULL OR (
ro.refund_no ILIKE '%' || p_search || '%' OR
o.order_no ILIKE '%' || p_search || '%'
))
ORDER BY ro.applied_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;-- =====================================================================================
-- Admin 订单功能 - 核销记录列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_write_off_record_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_search TEXT DEFAULT NULL,
p_verified_only BOOLEAN DEFAULT TRUE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数(核销订单类型 = 3
SELECT COUNT(*) INTO v_total
FROM public.ml_orders o
WHERE o.order_type = 3
AND (p_verified_only = FALSE OR o.verified_at IS NOT NULL)
AND (p_search IS NULL OR o.order_no ILIKE '%' || p_search || '%');
-- 3. 获取明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
o.id,
o.order_no,
o.total_amount,
o.payment_status,
o.order_status,
o.created_at,
o.verified_at,
buyer.username as customer_name,
buyer.phone as customer_phone,
verifier.username as verifier_name,
(
SELECT jsonb_build_object(
'product_name', oi.product_name,
'image_url', oi.image_url
)
FROM public.ml_order_items oi
WHERE oi.order_id = o.id
LIMIT 1
) as product_summary
FROM public.ml_orders o
LEFT JOIN public.ak_users buyer ON o.user_id = buyer.id
LEFT JOIN public.ak_users verifier ON o.verifier_id = verifier.id
WHERE o.order_type = 3
AND (p_verified_only = FALSE OR o.verified_at IS NOT NULL)
AND (p_search IS NULL OR o.order_no ILIKE '%' || p_search || '%')
ORDER BY o.verified_at DESC NULLS LAST, o.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;-- =====================================================================================
-- Admin 商品模块 - 删除分类 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1支持级联软删除商品关联
-- 依赖ml_categories, ml_products, ak_users 表已存在
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_category_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('product:category:delete') THEN
RAISE EXCEPTION 'Permission denied: product:category:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 检查是否有子分类 (方案 1)
IF EXISTS (
SELECT 1 FROM public.ml_categories
WHERE parent_id = p_id AND deleted_at IS NULL
) THEN
RAISE EXCEPTION '请先删除该分类下的子分类';
END IF;
-- 4. 级联软删除:该分类下的所有商品
UPDATE public.ml_products
SET deleted_at = now(),
deleted_by = v_user_id
WHERE category_id = p_id AND deleted_at IS NULL;
-- 5. 执行软删除分类本身
UPDATE public.ml_categories
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
RETURN FOUND;
END;
$$;
-- RPC: rpc_admin_get_product_reviews
-- 作用:管理端分页获取商品评论列表,包含商品名称、用户名及规格
-- 位置docs/sql/30_rpc/product/rpc_admin_get_product_reviews_v1.sql
CREATE OR REPLACE FUNCTION public.rpc_admin_get_product_reviews(
p_search_product text DEFAULT NULL,
p_search_user text DEFAULT NULL,
p_status integer DEFAULT NULL,
p_start_time timestamptz DEFAULT NULL,
p_end_time timestamptz DEFAULT NULL,
p_page integer DEFAULT 1,
p_page_size integer DEFAULT 20
)
RETURNS TABLE (
id uuid,
product_id uuid,
product_name text,
product_image text,
user_id uuid,
username text,
rating integer,
content text,
merchant_reply text,
status integer,
created_at timestamptz,
total_count bigint
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_offset integer := (p_page - 1) * p_page_size;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE ak_users.id = auth.uid() AND ak_users.role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
RETURN QUERY
WITH filtered_reviews AS (
SELECT
r.*,
p.name as p_name,
p.main_image_url as p_image,
u.username as u_name,
COUNT(*) OVER() as full_count
FROM public.ml_product_reviews r
LEFT JOIN public.ml_products p ON r.product_id = p.id
LEFT JOIN public.ak_users u ON r.user_id = u.id
WHERE (p_search_product IS NULL OR p.name ILIKE '%' || p_search_product || '%')
AND (p_search_user IS NULL OR u.username ILIKE '%' || p_search_user || '%')
AND (p_status IS NULL OR r.status = p_status)
AND (p_start_time IS NULL OR r.created_at >= p_start_time)
AND (p_end_time IS NULL OR r.created_at <= p_end_time)
)
SELECT
fr.id,
fr.product_id,
fr.p_name as product_name,
fr.p_image as product_image,
fr.user_id,
fr.u_name as username,
fr.rating,
fr.content,
fr.merchant_reply,
fr.status,
fr.created_at,
fr.full_count as total_count
FROM filtered_reviews fr
ORDER BY fr.created_at DESC
LIMIT p_page_size OFFSET v_offset;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.rpc_admin_get_product_reviews(text, text, integer, timestamptz, timestamptz, integer, integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.rpc_admin_get_product_reviews(text, text, integer, timestamptz, timestamptz, integer, integer) TO authenticated;
-- =====================================================================================
-- Admin 商品模块 - 商品统计概况 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_products, ml_orders, ml_browse_history, ak_users
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_product_stats(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_stats JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 统计核心指标
-- 商品浏览量 (PV), 访客数 (UV), 支付件数, 支付金额, 退款件数, 退款金额
WITH stats AS (
SELECT
(SELECT COALESCE(SUM(browse_duration), 0) FROM public.ml_browse_history WHERE created_at BETWEEN p_start_time AND p_end_time) as total_views,
(SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history WHERE created_at BETWEEN p_start_time AND p_end_time) as total_visitors,
(SELECT COALESCE(SUM(quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
WHERE o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_count,
(SELECT COALESCE(SUM(paid_amount), 0) FROM public.ml_orders
WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status NOT IN (1, 5)) as pay_amount,
(SELECT COUNT(*) FROM public.ml_orders WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status = 7) as refund_count,
(SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status = 7) as refund_amount
)
SELECT jsonb_build_object(
'views', total_views,
'visitors', total_visitors,
'pay_count', pay_count,
'pay_amount', pay_amount,
'refund_count', refund_count,
'refund_amount', refund_amount
) INTO v_stats FROM stats;
RETURN v_stats;
END;
$$;
-- =====================================================================================
-- Admin 商品模块 - 商品排行 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_product_ranking(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE,
p_sort_by TEXT DEFAULT 'sales', -- views, sales, amount
p_limit INTEGER DEFAULT 10
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取排行数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
p.id,
p.name,
p.main_image_url as image,
COALESCE(p.view_count, 0) as views,
(SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history bh WHERE bh.product_id = p.id AND bh.created_at BETWEEN p_start_time AND p_end_time) as visitors,
(SELECT COALESCE(SUM(quantity), 0) FROM public.ml_shopping_cart sc WHERE sc.product_id = p.id AND sc.created_at BETWEEN p_start_time AND p_end_time) as cart_count,
(SELECT COUNT(DISTINCT o.id) FROM public.ml_orders o JOIN public.ml_order_items oi ON o.id = oi.order_id
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time) as order_count,
(SELECT COALESCE(SUM(oi.quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_count,
(SELECT COALESCE(SUM(oi.total_amount), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_amount,
(SELECT COUNT(*) FROM public.ml_user_favorites f WHERE f.target_id = p.id AND f.target_type = 1 AND f.created_at BETWEEN p_start_time AND p_end_time) as fav_count
FROM public.ml_products p
WHERE p.status != 4
ORDER BY
CASE
WHEN p_sort_by = 'views' THEN COALESCE(p.view_count, 0)
WHEN p_sort_by = 'sales' THEN (
SELECT COALESCE(SUM(oi.quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)
)
WHEN p_sort_by = 'amount' THEN (
SELECT COALESCE(SUM(oi.total_amount), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)
)
ELSE COALESCE(p.view_count, 0)
END DESC
LIMIT p_limit
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- Admin 商品管理 - 商品状态汇总统计 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:统计出售中、仓库中、草稿箱、回收站各状态的商品数量
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_product_count_stats()
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_result JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 统计各状态数量
-- status 定义1:上架(出售中), 2:下架(仓库中), 3:草稿, 4:逻辑删除(回收站)
SELECT jsonb_build_object(
'selling', COUNT(*) FILTER (WHERE status = 1),
'warehouse', COUNT(*) FILTER (WHERE status = 2),
'draft', COUNT(*) FILTER (WHERE status = 3),
'recycle', COUNT(*) FILTER (WHERE status = 4)
) INTO v_result
FROM public.ml_products;
RETURN v_result;
END;
$$;
-- =====================================================================================
-- Admin 商品统计 - 营业趋势统计 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 说明:按天聚合指定时间范围内的商品浏览量、访客量、支付金额及退款金额
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_product_trend(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 按日聚合统计
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
to_char(date_trunc('day', gs.day), 'YYYY-MM-DD') AS date_group,
(SELECT COUNT(*) FROM public.ml_browse_history bh WHERE date_trunc('day', bh.created_at) = gs.day) as views,
(SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history bh WHERE date_trunc('day', bh.created_at) = gs.day) as visitors,
(SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders o WHERE date_trunc('day', o.created_at) = gs.day AND o.order_status NOT IN (1, 5)) as pay_amount,
(SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders o WHERE date_trunc('day', o.created_at) = gs.day AND o.order_status = 7) as refund_amount
FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
ORDER BY gs.day ASC
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_group_delete
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户分组(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('user:group:delete') THEN
RAISE EXCEPTION 'Permission denied: user:group:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 逻辑删除
UPDATE public.ak_user_groups
SET deleted_at = now(),
updated_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_group_list
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取用户分组列表,支持搜索、状态筛选及逻辑删除过滤
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_list(
p_page INT,
p_page_size INT,
p_search TEXT DEFAULT NULL,
p_status INT DEFAULT NULL,
p_include_deleted BOOLEAN DEFAULT FALSE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_total INT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ak_user_groups
WHERE (p_include_deleted OR deleted_at IS NULL)
AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%')
AND (p_status IS NULL OR status = p_status);
-- 3. 分页获取数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, name, remark, status,
created_at, updated_at, deleted_at
FROM public.ak_user_groups
WHERE (p_include_deleted OR deleted_at IS NULL)
AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%')
AND (p_status IS NULL OR status = p_status)
ORDER BY created_at DESC
LIMIT p_page_size
OFFSET (p_page - 1) * p_page_size
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_group_save
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:新增/更新用户分组(逻辑删除记录默认不允许更新)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_save(
p_id UUID DEFAULT NULL,
p_name TEXT,
p_remark TEXT DEFAULT NULL,
p_status INT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 参数校验
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
RAISE EXCEPTION 'Invalid name';
END IF;
-- 3. 新增
IF p_id IS NULL THEN
INSERT INTO public.ak_user_groups(
name, remark, status,
created_at, updated_at, deleted_at
) VALUES (
p_name, p_remark, COALESCE(p_status, 1),
now(), now(), NULL
)
RETURNING id INTO v_id;
RETURN v_id;
END IF;
-- 4. 更新(不允许更新已删除记录)
UPDATE public.ak_user_groups
SET
name = p_name,
remark = p_remark,
status = COALESCE(p_status, status),
updated_at = now()
WHERE id = p_id AND deleted_at IS NULL
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Not found or deleted';
END IF;
RETURN v_id;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_group_set_status
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:设置用户分组状态(启用/禁用)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_set_status(
p_id UUID,
p_status INT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
UPDATE public.ak_user_groups
SET status = p_status,
updated_at = now()
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_label_delete
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户标签(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('user:label:delete') THEN
RAISE EXCEPTION 'Permission denied: user:label:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 逻辑删除
UPDATE public.ak_user_labels
SET deleted_at = now(),
updated_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_label_list
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取用户标签列表,支持搜索、状态筛选及逻辑删除过滤
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_list(
p_page INT,
p_page_size INT,
p_search TEXT DEFAULT NULL,
p_status INT DEFAULT NULL,
p_include_deleted BOOLEAN DEFAULT FALSE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_total INT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ak_user_labels
WHERE (p_include_deleted OR deleted_at IS NULL)
AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%')
AND (p_status IS NULL OR status = p_status);
-- 3. 分页获取数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, name, color, remark, status,
created_at, updated_at, deleted_at
FROM public.ak_user_labels
WHERE (p_include_deleted OR deleted_at IS NULL)
AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%')
AND (p_status IS NULL OR status = p_status)
ORDER BY created_at DESC
LIMIT p_page_size
OFFSET (p_page - 1) * p_page_size
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_label_save
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:新增/更新用户标签(逻辑删除记录默认不允许更新)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_save(
p_id UUID DEFAULT NULL,
p_name TEXT,
p_color TEXT DEFAULT NULL,
p_remark TEXT DEFAULT NULL,
p_status INT DEFAULT 1
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 参数校验
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
RAISE EXCEPTION 'Invalid name';
END IF;
-- 3. 新增
IF p_id IS NULL THEN
INSERT INTO public.ak_user_labels(
name, color, remark, status,
created_at, updated_at, deleted_at
) VALUES (
p_name, p_color, p_remark, COALESCE(p_status, 1),
now(), now(), NULL
)
RETURNING id INTO v_id;
RETURN v_id;
END IF;
-- 4. 更新(不允许更新已删除记录)
UPDATE public.ak_user_labels
SET
name = p_name,
color = p_color,
remark = p_remark,
status = COALESCE(p_status, status),
updated_at = now()
WHERE id = p_id AND deleted_at IS NULL
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Not found or deleted';
END IF;
RETURN v_id;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_label_set_status
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:设置用户标签状态(启用/禁用)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_set_status(
p_id UUID,
p_status INT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
UPDATE public.ak_user_labels
SET status = p_status,
updated_at = now()
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_delete
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户等级(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT public.check_admin_permission('user:level:delete') THEN
RAISE EXCEPTION 'Permission denied: user:level:delete';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 逻辑删除
UPDATE public.ak_user_levels
SET deleted_at = now(),
updated_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_list
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端分页获取用户等级列表,支持搜索、状态筛选及逻辑删除过滤
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_list(
p_page INT,
p_page_size INT,
p_search TEXT DEFAULT NULL,
p_status INT DEFAULT NULL,
p_is_visible BOOLEAN DEFAULT NULL,
p_include_deleted BOOLEAN DEFAULT FALSE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_total INT;
v_items JSONB;
BEGIN
-- 1. 权限检查 (依赖 public.get_current_user_role())
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ak_user_levels
WHERE (p_include_deleted OR deleted_at IS NULL)
AND (p_search IS NULL OR name ILIKE '%' || p_search || '%')
AND (p_status IS NULL OR status = p_status)
AND (p_is_visible IS NULL OR is_visible = p_is_visible);
-- 3. 分页获取数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
id, name, level_weight, min_experience, discount_percent,
is_visible, status, icon_url, bg_image_url, bg_style_json,
remark, created_at, updated_at, deleted_at
FROM public.ak_user_levels
WHERE (p_include_deleted OR deleted_at IS NULL)
AND (p_search IS NULL OR name ILIKE '%' || p_search || '%')
AND (p_status IS NULL OR status = p_status)
AND (p_is_visible IS NULL OR is_visible = p_is_visible)
ORDER BY level_weight ASC
LIMIT p_page_size
OFFSET (p_page - 1) * p_page_size
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_save
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:新增/更新用户等级(逻辑删除记录默认不允许更新)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_save(
p_id UUID DEFAULT NULL,
p_name TEXT,
p_level_weight INT,
p_min_experience INT,
p_discount_percent INT,
p_is_visible BOOLEAN,
p_status INT,
p_icon_url TEXT DEFAULT NULL,
p_bg_image_url TEXT DEFAULT NULL,
p_bg_style_json JSONB DEFAULT NULL,
p_remark TEXT DEFAULT NULL
)
RETURNS UUID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 参数校验(最小化)
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
RAISE EXCEPTION 'Invalid name';
END IF;
IF p_level_weight < 0 OR p_min_experience < 0 THEN
RAISE EXCEPTION 'Invalid level_weight or min_experience';
END IF;
IF p_discount_percent < 1 OR p_discount_percent > 100 THEN
RAISE EXCEPTION 'Invalid discount_percent';
END IF;
-- 3. 新增
IF p_id IS NULL THEN
INSERT INTO public.ak_user_levels(
name, level_weight, min_experience, discount_percent,
is_visible, status,
icon_url, bg_image_url, bg_style_json,
remark,
created_at, updated_at, deleted_at
) VALUES (
p_name, p_level_weight, p_min_experience, p_discount_percent,
p_is_visible, p_status,
p_icon_url, p_bg_image_url, p_bg_style_json,
p_remark,
now(), now(), NULL
)
RETURNING id INTO v_id;
RETURN v_id;
END IF;
-- 4. 更新(不允许更新已删除记录)
UPDATE public.ak_user_levels
SET
name = p_name,
level_weight = p_level_weight,
min_experience = p_min_experience,
discount_percent = p_discount_percent,
is_visible = p_is_visible,
status = p_status,
icon_url = p_icon_url,
bg_image_url = p_bg_image_url,
bg_style_json = p_bg_style_json,
remark = p_remark,
updated_at = now()
WHERE id = p_id AND deleted_at IS NULL
RETURNING id INTO v_id;
IF v_id IS NULL THEN
RAISE EXCEPTION 'Not found or deleted';
END IF;
RETURN v_id;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_set_status
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:设置用户等级状态(启用/禁用)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_set_status(
p_id UUID,
p_status INT
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
UPDATE public.ak_user_levels
SET status = p_status,
updated_at = now()
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
-- =====================================================================================
-- RPC: rpc_admin_user_level_set_visible
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:设置用户等级是否展示
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_set_visible(
p_id UUID,
p_is_visible BOOLEAN
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
UPDATE public.ak_user_levels
SET is_visible = p_is_visible,
updated_at = now()
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;