522 lines
18 KiB
PL/PgSQL
522 lines
18 KiB
PL/PgSQL
-- 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 '创建分析功能的测试数据(开发用)';
|