sql数据流,amdin业务逻辑接入

This commit is contained in:
comlibmb
2026-02-05 10:11:09 +08:00
parent 859372ca5b
commit ac670cf5d8
81 changed files with 3547 additions and 1472 deletions

View File

@@ -0,0 +1,26 @@
-- =====================================================================================
-- Admin 用户统计模块 - 权限与依赖函数验证(测试/回归)
-- 目的:在部署/联调前快速确认 get_current_user_role 与性别分布 RPC 是否存在
-- =====================================================================================
-- 1) 检查 get_current_user_role 是否存在(权威依赖)
SELECT n.nspname AS schema, p.proname AS name
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE p.proname = 'get_current_user_role'
ORDER BY 1, 2;
-- 2) 检查性别分布 RPC 是否存在
SELECT n.nspname AS schema, p.proname AS name
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE p.proname = 'rpc_analytics_user_gender_distribution'
ORDER BY 1, 2;
-- 3) 字段检查ak_users 是否存在 auth_id 与 role供 get_current_user_role 使用)
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema='public'
AND table_name='ak_users'
AND column_name IN ('auth_id','role')
ORDER BY column_name;

View File

@@ -0,0 +1,118 @@
-- =====================================================================================
-- Admin 用户统计模块 - RPC 验证与调用样例
-- 说明: 本模块复用 analytics 模块的 RPC 函数,不重复定义。
-- 本文件用于验证 RPC 可用性,并提供前端调用示例。
-- 依赖: public.rpc_analytics_user_kpis, rpc_analytics_user_growth_trend 等
-- =====================================================================================
-- 1) 验证 RPC 函数是否存在
SELECT routine_name, routine_type
FROM information_schema.routines
WHERE routine_schema = 'public'
AND routine_name IN (
'rpc_analytics_user_kpis',
'rpc_analytics_user_growth_trend',
'rpc_analytics_user_segments',
'rpc_analytics_traffic_sources'
)
ORDER BY routine_name;
-- 2) 调用样例:获取用户统计 KPI最近 7 天)
-- 参数说明:
-- p_start_date: 起始日期 (DATE)
-- p_end_date: 结束日期 (DATE)
-- 返回字段:
-- total_users, user_growth, new_users, new_user_growth,
-- active_users, active_growth, ordering_users, ordering_growth,
-- paid_users, paid_growth, new_user_conversion_rate,
-- repurchase_rate, repurchase_growth
SELECT * FROM public.rpc_analytics_user_kpis(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
);
-- 3) 调用样例:获取用户增长与活跃趋势(最近 30 天,日维度)
-- 参数说明:
-- p_start_date: 起始日期 (DATE)
-- p_end_date: 结束日期 (DATE)
-- 返回字段:
-- date, new_users, active_users
SELECT * FROM public.rpc_analytics_user_growth_trend(
CURRENT_DATE - INTERVAL '29 days',
CURRENT_DATE
)
ORDER BY date;
-- 4) 调用样例:获取用户分群(新客/复购/老客)
-- 参数说明:
-- p_start_date: 起始日期 (DATE)
-- p_end_date: 结束日期 (DATE)
-- 返回字段:
-- name, value
SELECT * FROM public.rpc_analytics_user_segments(
CURRENT_DATE - INTERVAL '29 days',
CURRENT_DATE
);
-- 5) 调用样例:获取渠道来源分布(基于注册来源)
-- 参数说明:
-- p_start_date: 起始日期 (DATE)
-- p_end_date: 结束日期 (DATE)
-- 返回字段:
-- name, value
SELECT * FROM public.rpc_analytics_traffic_sources(
CURRENT_DATE - INTERVAL '29 days',
CURRENT_DATE
)
ORDER BY value DESC;
-- 6) 综合验证:一次性调用所有 RPC 并检查返回是否正常
DO $$
DECLARE
kpis RECORD;
trend RECORD;
seg RECORD;
src RECORD;
BEGIN
-- 验证 KPI RPC
FOR kpis IN SELECT * FROM public.rpc_analytics_user_kpis(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
) LIMIT 1
LOOP
RAISE NOTICE 'rpc_analytics_user_kpis OK: total_users=%, new_users=%, active_users=%',
kpis.total_users, kpis.new_users, kpis.active_users;
END LOOP;
-- 验证趋势 RPC
FOR trend IN SELECT * FROM public.rpc_analytics_user_growth_trend(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
) LIMIT 1
LOOP
RAISE NOTICE 'rpc_analytics_user_growth_trend OK: date=%, new_users=%, active_users=%',
trend.date, trend.new_users, trend.active_users;
END LOOP;
-- 验证分群 RPC
FOR seg IN SELECT * FROM public.rpc_analytics_user_segments(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
) LIMIT 3
LOOP
RAISE NOTICE 'rpc_analytics_user_segments OK: name=%, value=%',
seg.name, seg.value;
END LOOP;
-- 验证渠道 RPC
FOR src IN SELECT * FROM public.rpc_analytics_traffic_sources(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
) LIMIT 3
LOOP
RAISE NOTICE 'rpc_analytics_traffic_sources OK: name=%, value=%',
src.name, src.value;
END LOOP;
RAISE NOTICE '所有 RPC 验证通过。';
END $$;

View File

@@ -0,0 +1,95 @@
-- =====================================================================================
-- Admin 用户统计模块 - 性别比例 RPC新增
-- 说明: 基于 ak_users.gender 字段统计性别分布,兼容“未知”情况
-- 依赖: public.ak_users
-- =====================================================================================
-- 1) 检查 ak_users.gender 字段是否存在
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'ak_users'
AND column_name = 'gender'
ORDER BY column_name;
-- 2) RPC: rpc_analytics_user_gender_distribution
-- 参数: p_start_date, p_end_date (可选,用于统计周期内新增/活跃用户的性别分布)
-- 返回: name, value (性别名称与人数)
CREATE OR REPLACE FUNCTION public.rpc_analytics_user_gender_distribution(
p_start_date DATE DEFAULT NULL,
p_end_date DATE DEFAULT NULL
)
RETURNS TABLE (
name TEXT,
value BIGINT
)
LANGUAGE plpgsql
AS $$
DECLARE
has_gender BOOLEAN := FALSE;
where_clause TEXT := '';
BEGIN
-- 检查 gender 字段是否存在
SELECT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'ak_users'
AND column_name = 'gender'
) INTO has_gender;
IF NOT has_gender THEN
-- 如果没有 gender 字段,返回全“未知”
RETURN QUERY
SELECT '未知'::TEXT AS name,
COUNT(*)::BIGINT AS value
FROM public.ak_users
WHERE (p_start_date IS NULL OR p_end_date IS NULL) OR
(created_at::DATE BETWEEN COALESCE(p_start_date, '1970-01-01') AND COALESCE(p_end_date, CURRENT_DATE));
RETURN;
END IF;
-- 构建时间过滤条件
IF p_start_date IS NOT NULL AND p_end_date IS NOT NULL THEN
where_clause := ' AND created_at::DATE BETWEEN ''' || p_start_date::TEXT || ''' AND ''' || p_end_date::TEXT || '''';
END IF;
-- 动态查询:按 gender 分组统计,兼容 NULL/空值
RETURN QUERY EXECUTE '
SELECT
CASE
WHEN gender IS NULL OR gender = '''' OR LOWER(TRIM(gender)) IN (''null'', ''unknown'', ''未知'') THEN ''未知''
WHEN LOWER(TRIM(gender)) IN (''male'', ''m'', '''', ''1'') THEN ''''
WHEN LOWER(TRIM(gender)) IN (''female'', ''f'', '''', ''2'') THEN ''''
ELSE ''其他''
END AS name,
COUNT(*)::BIGINT AS value
FROM public.ak_users
WHERE 1=1' || where_clause || '
GROUP BY name
ORDER BY value DESC
';
END;
$$;
-- 3) 快速验证:调用 RPC全量用户
SELECT * FROM public.rpc_analytics_user_gender_distribution();
-- 4) 验证按月份调用示例2026-01
SELECT * FROM public.rpc_analytics_user_gender_distribution(
'2026-01-01',
'2026-01-31'
);
-- 5) 数据完整性检查:查看当前 gender 字段的分布情况(用于调试)
SELECT
gender,
COUNT(*) AS cnt,
ROUND(COUNT(*) * 100.0 / NULLIF((SELECT COUNT(*) FROM public.ak_users), 0), 2) AS pct
FROM public.ak_users
GROUP BY gender
ORDER BY cnt DESC;

View File

@@ -0,0 +1,126 @@
-- =====================================================================================
-- Admin 用户统计模块 - 完整验证脚本
-- 说明: 验证 RPC 可用性 + 模拟前端调用参数,确保返回数据结构与前端期望一致
-- =====================================================================================
-- 1) 检查 RPC 是否已部署(与 analytics 共享)
SELECT routine_name, routine_type
FROM information_schema.routines
WHERE routine_schema = 'public'
AND routine_name IN (
'rpc_analytics_user_kpis',
'rpc_analytics_user_growth_trend',
'rpc_analytics_user_segments',
'rpc_analytics_traffic_sources'
)
ORDER BY routine_name;
-- 2) 模拟前端调用:最近 30 天(与页面默认一致)
-- 2.1) KPI
SELECT * FROM public.rpc_analytics_user_kpis(
CURRENT_DATE - INTERVAL '29 days',
CURRENT_DATE
);
-- 2.2) 趋势(日维度)
SELECT * FROM public.rpc_analytics_user_growth_trend(
CURRENT_DATE - INTERVAL '29 days',
CURRENT_DATE
)
ORDER BY date;
-- 3) 模拟前端调用:最近 7 天(快速验证)
-- 3.1) KPI
SELECT * FROM public.rpc_analytics_user_kpis(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
);
-- 3.2) 趋势
SELECT * FROM public.rpc_analytics_user_growth_trend(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
)
ORDER BY date;
-- 4) 检查依赖表是否存在
SELECT table_name, table_type
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('ak_users', 'ml_orders', 'ml_browse_history')
ORDER BY table_name;
-- 5) 检查关键字段是否存在(避免 RPC 运行时报错)
SELECT
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'ak_users'
AND column_name IN ('id', 'created_at', 'registration_source')
ORDER BY column_name;
SELECT
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'ml_orders'
AND column_name IN ('id', 'user_id', 'created_at', 'payment_status')
ORDER BY column_name;
SELECT
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'ml_browse_history'
AND column_name IN ('user_id', 'created_at')
ORDER BY column_name;
-- 6) 数据完整性检查(确保有数据可查)
SELECT
(SELECT COUNT(*) FROM public.ak_users) AS ak_users_cnt,
(SELECT COUNT(*) FROM public.ml_orders) AS ml_orders_cnt,
(SELECT COUNT(*) FROM public.ml_browse_history) AS ml_browse_history_cnt;
-- 7) 快速验证脚本(返回是否可用)
DO $$
DECLARE
kpis_ok BOOLEAN := FALSE;
trend_ok BOOLEAN := FALSE;
BEGIN
-- 验证 KPI RPC
BEGIN
PERFORM 1 FROM public.rpc_analytics_user_kpis(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
) LIMIT 1;
kpis_ok := TRUE;
EXCEPTION WHEN OTHERS THEN
kpis_ok := FALSE;
END;
-- 验证趋势 RPC
BEGIN
PERFORM 1 FROM public.rpc_analytics_user_growth_trend(
CURRENT_DATE - INTERVAL '6 days',
CURRENT_DATE
) LIMIT 1;
trend_ok := TRUE;
EXCEPTION WHEN OTHERS THEN
trend_ok := FALSE;
END;
IF kpis_ok AND trend_ok THEN
RAISE NOTICE '✅ Admin 用户统计 RPC 验证通过';
ELSE
RAISE NOTICE '❌ Admin 用户统计 RPC 验证失败: kpis_ok=%, trend_ok=%', kpis_ok, trend_ok;
END IF;
END $$;

View File

@@ -0,0 +1,45 @@
-- Check if the function exists and show all overloaded versions
SELECT
proname as function_name,
pg_get_function_arguments(oid) as arguments,
pg_get_functiondef(oid) as definition
FROM pg_proc
WHERE pronamespace = 'public'::regnamespace
AND proname = 'rpc_analytics_user_kpis'
ORDER BY oid;
-- Alternative: Check function existence with more details
SELECT
n.nspname as schema_name,
p.proname as function_name,
pg_get_function_arguments(p.oid) as arguments,
pg_get_function_result(p.oid) as return_type,
p.prokind as function_type,
CASE p.prokind
WHEN 'f' THEN 'Function'
WHEN 'p' THEN 'Procedure'
WHEN 'a' THEN 'Aggregate'
WHEN 'w' THEN 'Window'
END as kind
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.proname = 'rpc_analytics_user_kpis'
AND n.nspname = 'public'
ORDER BY p.oid;
-- Check all functions in public schema
SELECT
proname as function_name,
pg_get_function_arguments(oid) as arguments
FROM pg_proc
WHERE pronamespace = 'public'::regnamespace
ORDER BY proname;
-- Quick existence check
SELECT
EXISTS(
SELECT 1
FROM pg_proc
WHERE pronamespace = 'public'::regnamespace
AND proname = 'rpc_analytics_user_kpis'
) as function_exists;

View File

@@ -0,0 +1,112 @@
-- =====================================================================================
-- RPC: rpc_admin_user_detail
-- Version: v1
-- Purpose: 管理后台用户详情(单个用户),返回完整资料+会员信息+地址
-- Security: SECURITY DEFINER + 固定 search_path + 入口鉴权admin/analytics
-- Depends:
-- - public.ak_users (auth_id, email, username, role, created_at, updated_at)
-- - public.ml_user_profiles (user_id, status, real_name, credit_score, verification_status, preferences, emergency_contact, service_areas)
-- - public.ml_user_addresses (user_id, receiver_name, receiver_phone, province, city, district, address_detail, is_default)
-- - public.ml_user_subscriptions (user_id, plan_id, status, start_date, end_date, next_billing_date, auto_renew)
-- - public.ml_subscription_plans (id, name, billing_period, price, features)
-- - public.get_current_user_role() (鉴权入口)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_detail(
p_user_id UUID
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_detail JSONB;
BEGIN
-- 1) 入口鉴权:仅允许 admin/analytics 角色调用
IF get_current_user_role() NOT IN ('admin', 'analytics') THEN
RAISE EXCEPTION 'Permission denied: required role admin or analytics';
END IF;
-- 2) 查询用户完整信息
SELECT jsonb_build_object(
'id', u.id,
'auth_id', u.auth_id,
'username', u.username,
'email', u.email,
'role', u.role,
'profile_status', up.status,
'real_name', up.real_name,
'credit_score', up.credit_score,
'verification_status', up.verification_status,
'preferences', up.preferences,
'emergency_contact', up.emergency_contact,
'service_areas', up.service_areas,
'created_at', u.created_at,
'updated_at', u.updated_at,
'balance', 0, -- 按要求固定返回 0
'is_member', COALESCE(member_info.is_member, false),
'member_info', member_info.subscription_detail,
'addresses', address_info.addresses
) INTO v_user_detail
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles up ON u.id = up.user_id
LEFT JOIN LATERAL (
SELECT
true as is_member,
jsonb_build_object(
'plan_name', p.name,
'plan_code', p.plan_code,
'billing_period', p.billing_period,
'price', p.price,
'features', p.features,
'status', s.status,
'start_date', s.start_date,
'end_date', s.end_date,
'next_billing_date', s.next_billing_date,
'auto_renew', s.auto_renew
) as subscription_detail
FROM public.ml_user_subscriptions s
JOIN public.ml_subscription_plans p ON s.plan_id = p.id
WHERE s.user_id = u.auth_id
AND s.status IN ('trial', 'active')
AND (s.end_date IS NULL OR s.end_date >= now())
ORDER BY s.end_date DESC NULLS FIRST
LIMIT 1
) member_info ON true
LEFT JOIN LATERAL (
SELECT jsonb_agg(
jsonb_build_object(
'id', a.id,
'receiver_name', a.receiver_name,
'receiver_phone', a.receiver_phone,
'province', a.province,
'city', a.city,
'district', a.district,
'address_detail', a.address_detail,
'postal_code', a.postal_code,
'is_default', a.is_default,
'label', a.label,
'latitude', a.latitude,
'longitude', a.longitude,
'delivery_instructions', a.delivery_instructions,
'business_hours', a.business_hours,
'status', a.status,
'created_at', a.created_at,
'updated_at', a.updated_at
) ORDER BY a.is_default DESC, a.created_at DESC
) as addresses
FROM public.ml_user_addresses a
WHERE a.user_id = u.id
AND a.status = 1
) address_info ON true
WHERE u.id = p_user_id;
-- 3) 用户不存在时返回 NULL
IF v_user_detail IS NULL THEN
RETURN NULL::jsonb;
END IF;
RETURN v_user_detail;
END;
$$;

View File

@@ -0,0 +1,156 @@
-- =====================================================================================
-- RPC: rpc_admin_user_list
-- Version: v1
-- Purpose: 管理后台用户列表(分页+筛选),返回基础资料+会员信息+余额(0)
-- Security: SECURITY DEFINER + 固定 search_path + 入口鉴权admin/analytics
-- Depends:
-- - public.ak_users (auth_id, email, username, role, created_at, updated_at)
-- - public.ml_user_profiles (user_id, status, real_name, credit_score, verification_status)
-- - public.ml_user_subscriptions (user_id, plan_id, status, end_date)
-- - public.ml_subscription_plans (id, name, billing_period, price)
-- - public.get_current_user_role() (鉴权入口)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_list(
p_page INTEGER DEFAULT 1,
p_limit INTEGER DEFAULT 20,
p_search TEXT DEFAULT NULL,
p_role TEXT DEFAULT NULL,
p_status INTEGER DEFAULT NULL,
p_is_member BOOLEAN DEFAULT NULL
)
RETURNS TABLE (
total BIGINT,
page INTEGER,
limit INTEGER,
has_more BOOLEAN,
items JSONB
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_offset INTEGER := (p_page - 1) * p_limit;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1) 入口鉴权:仅允许 admin/analytics 角色调用
IF get_current_user_role() NOT IN ('admin', 'analytics') THEN
RAISE EXCEPTION 'Permission denied: required role admin or analytics';
END IF;
-- 2) 计算总数(用于分页)
WITH filtered_users AS (
SELECT u.id, u.auth_id, u.email, u.username, u.role,
u.created_at, u.updated_at,
up.status as profile_status,
up.real_name,
up.credit_score,
up.verification_status
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles up ON u.id = up.user_id
WHERE 1=1
AND (p_search IS NULL OR (
u.username ILIKE '%' || p_search || '%' OR
u.email ILIKE '%' || p_search || '%' OR
up.real_name ILIKE '%' || p_search || '%'
))
AND (p_role IS NULL OR u.role = p_role)
AND (p_status IS NULL OR up.status = p_status)
AND (
p_is_member IS NULL OR
(p_is_member = TRUE AND EXISTS (
SELECT 1
FROM public.ml_user_subscriptions s
WHERE s.user_id = u.auth_id
AND s.status IN ('trial', 'active')
AND (s.end_date IS NULL OR s.end_date >= now())
)) OR
(p_is_member = FALSE AND NOT EXISTS (
SELECT 1
FROM public.ml_user_subscriptions s
WHERE s.user_id = u.auth_id
AND s.status IN ('trial', 'active')
AND (s.end_date IS NULL OR s.end_date >= now())
))
)
)
SELECT COUNT(*) INTO v_total FROM filtered_users;
-- 3) 查询分页数据并组装会员信息
SELECT jsonb_agg(
jsonb_build_object(
'id', u.id,
'auth_id', u.auth_id,
'username', u.username,
'email', u.email,
'role', u.role,
'profile_status', u.profile_status,
'real_name', u.real_name,
'credit_score', u.credit_score,
'verification_status', u.verification_status,
'created_at', u.created_at,
'updated_at', u.updated_at,
'balance', 0, -- 按要求固定返回 0
'is_member', COALESCE(member_info.is_member, false),
'member_plan_name', member_info.plan_name,
'member_end_date', member_info.end_date
)
) INTO v_items
FROM (
SELECT u.*, up.status as profile_status, up.real_name, up.credit_score, up.verification_status
FROM public.ak_users u
LEFT JOIN public.ml_user_profiles up ON u.id = up.user_id
WHERE 1=1
AND (p_search IS NULL OR (
u.username ILIKE '%' || p_search || '%' OR
u.email ILIKE '%' || p_search || '%' OR
up.real_name ILIKE '%' || p_search || '%'
))
AND (p_role IS NULL OR u.role = p_role)
AND (p_status IS NULL OR up.status = p_status)
AND (
p_is_member IS NULL OR
(p_is_member = TRUE AND EXISTS (
SELECT 1
FROM public.ml_user_subscriptions s
WHERE s.user_id = u.auth_id
AND s.status IN ('trial', 'active')
AND (s.end_date IS NULL OR s.end_date >= now())
)) OR
(p_is_member = FALSE AND NOT EXISTS (
SELECT 1
FROM public.ml_user_subscriptions s
WHERE s.user_id = u.auth_id
AND s.status IN ('trial', 'active')
AND (s.end_date IS NULL OR s.end_date >= now())
))
)
ORDER BY u.created_at DESC
LIMIT p_limit OFFSET v_offset
) u
LEFT JOIN LATERAL (
SELECT
true as is_member,
p.name as plan_name,
s.end_date
FROM public.ml_user_subscriptions s
JOIN public.ml_subscription_plans p ON s.plan_id = p.id
WHERE s.user_id = u.auth_id
AND s.status IN ('trial', 'active')
AND (s.end_date IS NULL OR s.end_date >= now())
ORDER BY s.end_date DESC NULLS FIRST
LIMIT 1
) member_info ON true;
-- 4) 返回分页结构
RETURN QUERY
SELECT
v_total,
p_page,
p_limit,
(v_offset + p_limit) < v_total as has_more,
COALESCE(v_items, '[]'::jsonb) as items;
END;
$$;