Files
akmon/doc_zhipao/analytics_rpc_functions.sql
2026-01-20 08:04:15 +08:00

522 lines
18 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Analytics 页面 RPC 函数 SQL 语句
-- 运动训练监测与AI评估平台 - 教师数据分析功能
-- 1. 获取教师统计数据 (get_teacher_analytics)
CREATE OR REPLACE FUNCTION public.get_teacher_analytics(
p_teacher_id uuid DEFAULT NULL,
p_start_date date DEFAULT NULL,
p_end_date date DEFAULT NULL
)
RETURNS TABLE(
total_students integer,
total_assignments integer,
completion_rate numeric,
average_score numeric,
active_classes integer,
total_submissions integer,
pending_reviews integer,
graded_submissions integer
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_teacher_id uuid;
v_start_date date;
v_end_date date;
BEGIN
-- 参数处理
v_teacher_id := COALESCE(p_teacher_id, auth.uid());
v_start_date := COALESCE(p_start_date, CURRENT_DATE - INTERVAL '30 days');
v_end_date := COALESCE(p_end_date, CURRENT_DATE);
-- 确保是教师用户
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = v_teacher_id AND role = 'teacher'
) THEN
RAISE EXCEPTION '用户不是教师或不存在';
END IF;
RETURN QUERY
WITH teacher_classes AS (
-- 获取教师关联的班级
SELECT DISTINCT uc.class_id
FROM public.ak_user_classes uc
WHERE uc.user_id = v_teacher_id
AND uc.role = 'teacher'
),
class_students AS (
-- 获取班级中的学生
SELECT DISTINCT uc.user_id as student_id, uc.class_id
FROM public.ak_user_classes uc
INNER JOIN teacher_classes tc ON uc.class_id = tc.class_id
WHERE uc.role = 'student'
),
teacher_assignments AS (
-- 获取教师的作业
SELECT a.*
FROM public.ak_assignments a
INNER JOIN teacher_classes tc ON a.class_id = tc.class_id
WHERE a.created_at::date BETWEEN v_start_date AND v_end_date
),
assignment_stats AS (
-- 作业统计
SELECT
COUNT(DISTINCT cs.student_id) as student_count,
COUNT(DISTINCT ta.id) as assignment_count,
COUNT(asub.id) as total_submission_count,
COUNT(CASE WHEN asub.score IS NOT NULL THEN 1 END) as graded_count,
COUNT(CASE WHEN asub.score IS NULL AND asub.id IS NOT NULL THEN 1 END) as pending_count,
AVG(CASE WHEN asub.score IS NOT NULL THEN asub.score END) as avg_score
FROM class_students cs
CROSS JOIN teacher_assignments ta
LEFT JOIN public.ak_assignment_submissions asub
ON ta.id = asub.assignment_id
AND cs.student_id = asub.student_id
)
SELECT
COALESCE(ast.student_count, 0)::integer,
COALESCE(ast.assignment_count, 0)::integer,
CASE
WHEN ast.student_count > 0 AND ast.assignment_count > 0
THEN ROUND((ast.total_submission_count::numeric / (ast.student_count * ast.assignment_count) * 100), 2)
ELSE 0
END,
COALESCE(ROUND(ast.avg_score, 2), 0),
(SELECT COUNT(DISTINCT class_id) FROM teacher_classes)::integer,
COALESCE(ast.total_submission_count, 0)::integer,
COALESCE(ast.pending_count, 0)::integer,
COALESCE(ast.graded_count, 0)::integer
FROM assignment_stats ast;
END;
$$;
-- 为函数添加注释
COMMENT ON FUNCTION public.get_teacher_analytics IS '获取教师统计数据:学生数、作业数、完成率、平均分等';
-- 2. 获取优秀学员排行 (get_top_performers)
CREATE OR REPLACE FUNCTION public.get_top_performers(
p_teacher_id uuid DEFAULT NULL,
p_start_date date DEFAULT NULL,
p_end_date date DEFAULT NULL,
p_limit integer DEFAULT 10
)
RETURNS TABLE(
student_id uuid,
name text,
username text,
avatar_url text,
score numeric,
submission_count integer,
completion_rate numeric,
class_name text,
rank_position integer
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_teacher_id uuid;
v_start_date date;
v_end_date date;
v_limit integer;
BEGIN
-- 参数处理
v_teacher_id := COALESCE(p_teacher_id, auth.uid());
v_start_date := COALESCE(p_start_date, CURRENT_DATE - INTERVAL '30 days');
v_end_date := COALESCE(p_end_date, CURRENT_DATE);
v_limit := COALESCE(p_limit, 10);
-- 确保是教师用户
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = v_teacher_id AND role = 'teacher'
) THEN
RAISE EXCEPTION '用户不是教师或不存在';
END IF;
RETURN QUERY
WITH teacher_classes AS (
-- 获取教师关联的班级
SELECT DISTINCT uc.class_id, c.name as class_name
FROM public.ak_user_classes uc
INNER JOIN public.ak_classes c ON uc.class_id = c.id
WHERE uc.user_id = v_teacher_id
AND uc.role = 'teacher'
),
class_students AS (
-- 获取班级中的学生
SELECT DISTINCT uc.user_id as student_id, uc.class_id, tc.class_name
FROM public.ak_user_classes uc
INNER JOIN teacher_classes tc ON uc.class_id = tc.class_id
WHERE uc.role = 'student'
),
teacher_assignments AS (
-- 获取教师的作业
SELECT a.*
FROM public.ak_assignments a
INNER JOIN teacher_classes tc ON a.class_id = tc.class_id
WHERE a.created_at::date BETWEEN v_start_date AND v_end_date
),
student_performance AS (
-- 学生表现统计
SELECT
cs.student_id,
cs.class_name,
u.username,
COALESCE(up.avatar_url, u.avatar_url) as avatar_url,
COUNT(asub.id) as submission_count,
COUNT(ta.id) as total_assignments,
AVG(CASE WHEN asub.score IS NOT NULL THEN asub.score END) as avg_score,
CASE
WHEN COUNT(ta.id) > 0
THEN ROUND((COUNT(asub.id)::numeric / COUNT(ta.id) * 100), 2)
ELSE 0
END as completion_rate
FROM class_students cs
INNER JOIN public.ak_users u ON cs.student_id = u.id
LEFT JOIN public.ak_user_profiles up ON u.id = up.user_id
CROSS JOIN teacher_assignments ta
LEFT JOIN public.ak_assignment_submissions asub
ON ta.id = asub.assignment_id
AND cs.student_id = asub.student_id
GROUP BY cs.student_id, cs.class_name, u.username, up.avatar_url, u.avatar_url
HAVING COUNT(asub.id) > 0 -- 只显示有提交记录的学生
),
ranked_students AS (
SELECT
sp.*,
ROW_NUMBER() OVER (
ORDER BY
COALESCE(sp.avg_score, 0) DESC,
sp.completion_rate DESC,
sp.submission_count DESC
) as rank_pos
FROM student_performance sp
)
SELECT
rs.student_id,
COALESCE(u.username, '未知学员') as name,
rs.username,
rs.avatar_url,
COALESCE(ROUND(rs.avg_score, 2), 0) as score,
rs.submission_count::integer,
rs.completion_rate,
rs.class_name,
rs.rank_pos::integer
FROM ranked_students rs
INNER JOIN public.ak_users u ON rs.student_id = u.id
WHERE rs.rank_pos <= v_limit
ORDER BY rs.rank_pos;
END;
$$;
-- 为函数添加注释
COMMENT ON FUNCTION public.get_top_performers IS '获取优秀学员排行榜:按平均分、完成率排序';
-- 3. 获取图表数据 (get_chart_data)
CREATE OR REPLACE FUNCTION public.get_chart_data(
p_teacher_id uuid DEFAULT NULL,
p_start_date date DEFAULT NULL,
p_end_date date DEFAULT NULL,
p_type text DEFAULT 'completion_rate'
)
RETURNS TABLE(
date_key date,
value numeric,
label text,
count integer
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_teacher_id uuid;
v_start_date date;
v_end_date date;
v_type text;
BEGIN
-- 参数处理
v_teacher_id := COALESCE(p_teacher_id, auth.uid());
v_start_date := COALESCE(p_start_date, CURRENT_DATE - INTERVAL '30 days');
v_end_date := COALESCE(p_end_date, CURRENT_DATE);
v_type := COALESCE(p_type, 'completion_rate');
-- 确保是教师用户
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = v_teacher_id AND role = 'teacher'
) THEN
RAISE EXCEPTION '用户不是教师或不存在';
END IF;
-- 根据类型返回不同的图表数据
IF v_type = 'completion_rate' THEN
-- 作业完成率趋势
RETURN QUERY
WITH teacher_classes AS (
SELECT DISTINCT uc.class_id
FROM public.ak_user_classes uc
WHERE uc.user_id = v_teacher_id AND uc.role = 'teacher'
),
date_series AS (
SELECT generate_series(v_start_date, v_end_date, '1 day'::interval)::date as date_key
),
daily_stats AS (
SELECT
ds.date_key,
COUNT(DISTINCT a.id) as assignments_due,
COUNT(asub.id) as submissions_made
FROM date_series ds
LEFT JOIN public.ak_assignments a ON ds.date_key = a.due_date::date
AND a.class_id IN (SELECT class_id FROM teacher_classes)
LEFT JOIN public.ak_assignment_submissions asub ON a.id = asub.assignment_id
AND asub.submit_time::date <= ds.date_key
GROUP BY ds.date_key
)
SELECT
dst.date_key,
CASE
WHEN dst.assignments_due > 0
THEN ROUND((dst.submissions_made::numeric / dst.assignments_due * 100), 2)
ELSE 0
END as value,
'完成率' as label,
dst.submissions_made::integer as count
FROM daily_stats dst
WHERE dst.assignments_due > 0 OR dst.submissions_made > 0
ORDER BY dst.date_key;
ELSIF v_type = 'score_distribution' THEN
-- 成绩分布
RETURN QUERY
WITH teacher_classes AS (
SELECT DISTINCT uc.class_id
FROM public.ak_user_classes uc
WHERE uc.user_id = v_teacher_id AND uc.role = 'teacher'
),
score_ranges AS (
SELECT
CASE
WHEN asub.score >= 90 THEN '90-100'
WHEN asub.score >= 80 THEN '80-89'
WHEN asub.score >= 70 THEN '70-79'
WHEN asub.score >= 60 THEN '60-69'
ELSE '60以下'
END as score_range,
asub.score
FROM public.ak_assignment_submissions asub
INNER JOIN public.ak_assignments a ON asub.assignment_id = a.id
INNER JOIN teacher_classes tc ON a.class_id = tc.class_id
WHERE asub.score IS NOT NULL
AND asub.submit_time::date BETWEEN v_start_date AND v_end_date
)
SELECT
v_start_date as date_key, -- 使用开始日期作为占位符
0::numeric as value, -- 不使用value字段
sr.score_range as label,
COUNT(*)::integer as count
FROM score_ranges sr
GROUP BY sr.score_range
ORDER BY
CASE sr.score_range
WHEN '90-100' THEN 1
WHEN '80-89' THEN 2
WHEN '70-79' THEN 3
WHEN '60-69' THEN 4
WHEN '60以下' THEN 5
END;
ELSIF v_type = 'submission_trend' THEN
-- 提交趋势
RETURN QUERY
WITH teacher_classes AS (
SELECT DISTINCT uc.class_id
FROM public.ak_user_classes uc
WHERE uc.user_id = v_teacher_id AND uc.role = 'teacher'
),
date_series AS (
SELECT generate_series(v_start_date, v_end_date, '1 day'::interval)::date as date_key
)
SELECT
ds.date_key,
COUNT(asub.id)::numeric as value,
'提交数量' as label,
COUNT(asub.id)::integer as count
FROM date_series ds
LEFT JOIN public.ak_assignment_submissions asub ON ds.date_key = asub.submit_time::date
LEFT JOIN public.ak_assignments a ON asub.assignment_id = a.id
LEFT JOIN teacher_classes tc ON a.class_id = tc.class_id
WHERE tc.class_id IS NOT NULL OR asub.id IS NULL
GROUP BY ds.date_key
ORDER BY ds.date_key;
ELSE
-- 默认返回空结果
RETURN;
END IF;
END;
$$;
-- 为函数添加注释
COMMENT ON FUNCTION public.get_chart_data IS '获取图表数据:支持完成率趋势、成绩分布、提交趋势等类型';
-- 4. 获取近期活动数据 (get_recent_activities) - 额外的辅助函数
CREATE OR REPLACE FUNCTION public.get_recent_activities(
p_teacher_id uuid DEFAULT NULL,
p_limit integer DEFAULT 20
)
RETURNS TABLE(
activity_id uuid,
activity_type text,
title text,
description text,
student_name text,
assignment_title text,
activity_time timestamp with time zone,
time_ago text
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_teacher_id uuid;
v_limit integer;
BEGIN
-- 参数处理
v_teacher_id := COALESCE(p_teacher_id, auth.uid());
v_limit := COALESCE(p_limit, 20);
-- 确保是教师用户
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = v_teacher_id AND role = 'teacher'
) THEN
RAISE EXCEPTION '用户不是教师或不存在';
END IF;
RETURN QUERY
WITH teacher_classes AS (
SELECT DISTINCT uc.class_id
FROM public.ak_user_classes uc
WHERE uc.user_id = v_teacher_id AND uc.role = 'teacher'
),
recent_submissions AS (
SELECT
asub.id as activity_id,
'assignment_submitted' as activity_type,
u.username || '提交了作业:' || a.title as title,
COALESCE(LEFT(asub.content_md, 100), '无描述') as description,
u.username as student_name,
a.title as assignment_title,
asub.submit_time as activity_time
FROM public.ak_assignment_submissions asub
INNER JOIN public.ak_assignments a ON asub.assignment_id = a.id
INNER JOIN public.ak_users u ON asub.student_id = u.id
INNER JOIN teacher_classes tc ON a.class_id = tc.class_id
WHERE asub.submit_time >= CURRENT_DATE - INTERVAL '7 days'
),
recent_assignments AS (
SELECT
a.id as activity_id,
'assignment_created' as activity_type,
'创建了新作业:' || a.title as title,
COALESCE(LEFT(a.description, 100), '无描述') as description,
'系统' as student_name,
a.title as assignment_title,
a.created_at as activity_time
FROM public.ak_assignments a
INNER JOIN teacher_classes tc ON a.class_id = tc.class_id
WHERE a.created_at >= CURRENT_DATE - INTERVAL '7 days'
AND a.teacher_id = v_teacher_id
),
all_activities AS (
SELECT * FROM recent_submissions
UNION ALL
SELECT * FROM recent_assignments
)
SELECT
aa.activity_id,
aa.activity_type,
aa.title,
aa.description,
aa.student_name,
aa.assignment_title,
aa.activity_time,
CASE
WHEN aa.activity_time > CURRENT_TIMESTAMP - INTERVAL '1 hour'
THEN EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - aa.activity_time))::integer / 60 || '分钟前'
WHEN aa.activity_time > CURRENT_TIMESTAMP - INTERVAL '1 day'
THEN EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - aa.activity_time))::integer / 3600 || '小时前'
ELSE EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - aa.activity_time))::integer / 86400 || '天前'
END as time_ago
FROM all_activities aa
ORDER BY aa.activity_time DESC
LIMIT v_limit;
END;
$$;
-- 为函数添加注释
COMMENT ON FUNCTION public.get_recent_activities IS '获取近期活动:作业提交、新作业创建等';
-- 5. 创建必要的索引以优化查询性能
CREATE INDEX IF NOT EXISTS idx_assignments_teacher_date ON public.ak_assignments(teacher_id, created_at);
CREATE INDEX IF NOT EXISTS idx_assignments_class_due ON public.ak_assignments(class_id, due_date);
CREATE INDEX IF NOT EXISTS idx_submissions_assignment_date ON public.ak_assignment_submissions(assignment_id, submit_time);
CREATE INDEX IF NOT EXISTS idx_submissions_student_score ON public.ak_assignment_submissions(student_id, score) WHERE score IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_user_classes_role ON public.ak_user_classes(user_id, class_id, role);
-- 6. 授权函数给authenticated用户
GRANT EXECUTE ON FUNCTION public.get_teacher_analytics TO authenticated;
GRANT EXECUTE ON FUNCTION public.get_top_performers TO authenticated;
GRANT EXECUTE ON FUNCTION public.get_chart_data TO authenticated;
GRANT EXECUTE ON FUNCTION public.get_recent_activities TO authenticated;
-- 7. 创建RLS策略确保数据安全
-- 这些函数已经在内部进行了权限检查但我们仍然可以为相关表添加RLS策略
-- 为作业表添加RLS策略如果还没有的话
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_policies
WHERE tablename = 'ak_assignments'
AND policyname = 'teachers_can_manage_own_assignments'
) THEN
CREATE POLICY "teachers_can_manage_own_assignments" ON public.ak_assignments
FOR ALL TO authenticated
USING (
teacher_id = auth.uid() OR
EXISTS (
SELECT 1 FROM public.ak_user_classes uc
WHERE uc.class_id = ak_assignments.class_id
AND uc.user_id = auth.uid()
AND uc.role = 'teacher'
)
);
END IF;
END;
$$;
-- 插入示例数据用于测试(可选)
-- INSERT INTO public.ak_languages (code, name, native_name, is_default) VALUES
-- ('zh-CN', 'Chinese Simplified', '简体中文', true),
-- ('en-US', 'English US', 'English (US)', false)
-- ON CONFLICT (code) DO NOTHING;
-- 创建测试数据的函数
CREATE OR REPLACE FUNCTION public.create_test_analytics_data()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
-- 这个函数可以用来创建一些测试数据
-- 实际部署时可以删除
RAISE NOTICE '测试数据创建函数已准备就绪';
END;
$$;
COMMENT ON FUNCTION public.create_test_analytics_data IS '创建分析功能的测试数据(开发用)';