6170 lines
192 KiB
PL/PgSQL
6170 lines
192 KiB
PL/PgSQL
-- =====================================================================================
|
||
-- 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;
|
||
$$;
|