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