Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

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