-- 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 '创建分析功能的测试数据(开发用)';