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