add class pk

This commit is contained in:
2026-01-03 21:52:02 +08:00
parent 3b0beb851f
commit 4bec76ab78
12 changed files with 344 additions and 0 deletions

0
class_pk/__init__.py Normal file
View File

3
class_pk/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

7
class_pk/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class ClassPkConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'class_pk'
verbose_name = '班级PK'

View File

@@ -0,0 +1,2 @@
# 空文件

4
class_pk/models.py Normal file
View File

@@ -0,0 +1,4 @@
from django.db import models
# 如果需要存储班级PK历史记录可以在这里定义模型
# 目前暂时不需要,因为都是实时计算

3
class_pk/serializers.py Normal file
View File

@@ -0,0 +1,3 @@
# 如果需要序列化器,可以在这里定义
# 目前使用APIView的paginate_data方法暂时不需要

View File

@@ -0,0 +1,2 @@
# 空文件

10
class_pk/urls/oj.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path
from ..views.oj import ClassRankAPI, UserClassRankAPI, ClassPKAPI
urlpatterns = [
path("class_rank", ClassRankAPI.as_view()),
path("user_class_rank", UserClassRankAPI.as_view()),
path("class_pk", ClassPKAPI.as_view()),
]

View File

311
class_pk/views/oj.py Normal file
View File

@@ -0,0 +1,311 @@
import statistics
from datetime import datetime
from django.db.models import Sum, Avg
from django.utils import timezone
from utils.api import APIView
from account.decorators import login_required
from account.models import User, UserProfile, AdminType
from submission.models import Submission, JudgeStatus
class ClassRankAPI(APIView):
"""获取班级排名列表"""
def get(self, request):
# 获取年级参数
grade = request.GET.get("grade")
# 获取所有有用户的班级
classes_query = User.objects.filter(
class_name__isnull=False,
is_disabled=False,
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN]
)
# 如果指定了年级,过滤班级名称以该年级开头的班级
if grade:
try:
grade = int(grade)
# 匹配以年级开头的班级名称,如 "22级"、"22"、"2022级" 等
classes_query = classes_query.filter(
class_name__startswith=str(grade)
)
except ValueError:
pass
classes = classes_query.values('class_name').distinct()
class_stats = []
for class_info in classes:
class_name = class_info['class_name']
users = User.objects.filter(
class_name=class_name,
is_disabled=False,
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN]
)
user_ids = list(users.values_list('id', flat=True))
profiles = UserProfile.objects.filter(user_id__in=user_ids)
total_ac = profiles.aggregate(
total=Sum('accepted_number')
)['total'] or 0
total_submission = profiles.aggregate(
total=Sum('submission_number')
)['total'] or 0
avg_ac = profiles.aggregate(
avg=Avg('accepted_number')
)['avg'] or 0
user_count = users.count()
class_stats.append({
'class_name': class_name,
'user_count': user_count,
'total_ac': int(total_ac),
'total_submission': int(total_submission),
'avg_ac': round(avg_ac, 2),
'ac_rate': round(total_ac / total_submission * 100, 2) if total_submission > 0 else 0
})
# 按总AC数排序
class_stats.sort(key=lambda x: (-x['total_ac'], x['total_submission']))
# 添加排名
for i, stat in enumerate(class_stats):
stat['rank'] = i + 1
# 手动实现分页因为class_stats是list不是QuerySet
try:
limit = int(request.GET.get("limit", "20"))
except ValueError:
limit = 20
if limit < 0 or limit > 250:
limit = 20
try:
offset = int(request.GET.get("offset", "0"))
except ValueError:
offset = 0
if offset < 0:
offset = 0
total = len(class_stats)
results = class_stats[offset:offset + limit]
return self.success({
"results": results,
"total": total
})
class UserClassRankAPI(APIView):
"""获取用户在班级中的排名"""
@login_required
def get(self, request):
user = request.user
if not user.class_name:
return self.error("用户没有班级信息")
# 获取同班所有用户
class_users = User.objects.filter(
class_name=user.class_name,
is_disabled=False,
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN]
).select_related('userprofile')
user_ranks = []
for class_user in class_users:
profile = class_user.userprofile
user_ranks.append({
'user_id': class_user.id,
'username': class_user.username,
'accepted_number': profile.accepted_number,
'submission_number': profile.submission_number,
})
# 按AC数排序
user_ranks.sort(key=lambda x: (-x['accepted_number'], x['submission_number']))
# 添加排名
my_rank = -1
for i, rank_info in enumerate(user_ranks):
rank_info['rank'] = i + 1
if rank_info['user_id'] == user.id:
my_rank = i + 1
return self.success({
'class_name': user.class_name,
'my_rank': my_rank,
'total_users': len(user_ranks),
'ranks': self.paginate_data(request, user_ranks, None)
})
class ClassPKAPI(APIView):
"""班级PK比较 - 多维度教育评价"""
def post(self, request):
class_names = request.data.get('class_name', [])
if not class_names or len(class_names) < 2:
return self.error("至少需要选择2个班级进行比较")
# 获取时间段参数
start_time = request.data.get("start_time")
end_time = request.data.get("end_time")
# 将时间字符串转换为datetime对象
# 处理空字符串、None 或 undefined 的情况
if start_time and isinstance(start_time, str) and start_time.strip():
try:
start_time = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
if timezone.is_naive(start_time):
start_time = timezone.make_aware(start_time)
except (ValueError, AttributeError):
start_time = None
else:
start_time = None
if end_time and isinstance(end_time, str) and end_time.strip():
try:
end_time = datetime.fromisoformat(end_time.replace('Z', '+00:00'))
if timezone.is_naive(end_time):
end_time = timezone.make_aware(end_time)
except (ValueError, AttributeError):
end_time = None
else:
end_time = None
class_comparisons = []
for class_name in class_names:
users = User.objects.filter(
class_name=class_name,
is_disabled=False,
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN]
)
user_ids = list(users.values_list('id', flat=True))
# 获取所有学生的AC数列表用于统计计算
profiles = UserProfile.objects.filter(user_id__in=user_ids)
ac_list = sorted([p.accepted_number for p in profiles], reverse=True)
submission_list = sorted([p.submission_number for p in profiles], reverse=True)
user_count = len(ac_list)
if user_count == 0:
continue
# 基础统计
total_ac = sum(ac_list)
total_submission = sum(submission_list)
avg_ac = statistics.mean(ac_list) if ac_list else 0
# 中位数和分位数
median_ac = statistics.median(ac_list) if ac_list else 0
q1_ac = statistics.quantiles(ac_list, n=4)[0] if len(ac_list) > 1 else 0
q3_ac = statistics.quantiles(ac_list, n=4)[2] if len(ac_list) > 1 else 0
iqr = q3_ac - q1_ac
# 标准差
std_dev = statistics.stdev(ac_list) if len(ac_list) > 1 else 0
# 前10名和后10名统计
top_10_count = min(10, user_count)
bottom_10_count = min(10, user_count)
top_10_avg = statistics.mean(ac_list[:top_10_count]) if top_10_count > 0 else 0
bottom_10_avg = statistics.mean(ac_list[-bottom_10_count:]) if bottom_10_count > 0 else 0
# 前25%和后25%统计
top_25_count = max(1, user_count // 4)
bottom_25_count = max(1, user_count // 4)
top_25_avg = statistics.mean(ac_list[:top_25_count]) if top_25_count > 0 else 0
bottom_25_avg = statistics.mean(ac_list[-bottom_25_count:]) if bottom_25_count > 0 else 0
# 优秀率AC数 >= 平均值的1.5倍)
excellent_threshold = avg_ac * 1.5
excellent_count = sum(1 for ac in ac_list if ac >= excellent_threshold)
excellent_rate = (excellent_count / user_count * 100) if user_count > 0 else 0
# 及格率AC数 >= 平均值的0.5倍)
pass_threshold = avg_ac * 0.5
pass_count = sum(1 for ac in ac_list if ac >= pass_threshold)
pass_rate = (pass_count / user_count * 100) if user_count > 0 else 0
# 参与度(有提交记录的学生比例)
active_count = sum(1 for sub in submission_list if sub > 0)
active_rate = (active_count / user_count * 100) if user_count > 0 else 0
# 时间段内的统计(如果提供了时间段)
recent_stats = {}
if start_time and end_time:
submissions = Submission.objects.filter(
user_id__in=user_ids,
create_time__gte=start_time,
create_time__lte=end_time
)
recent_ac = submissions.filter(
result=JudgeStatus.ACCEPTED
).values('user_id', 'problem_id').distinct().count()
recent_submission = submissions.count()
# 时间段内的用户AC数列表
recent_user_ac = {}
for user_id in user_ids:
user_recent_ac = submissions.filter(
user_id=user_id,
result=JudgeStatus.ACCEPTED
).values('problem_id').distinct().count()
recent_user_ac[user_id] = user_recent_ac
recent_ac_list = sorted(recent_user_ac.values(), reverse=True)
if recent_ac_list:
recent_stats = {
'recent_total_ac': recent_ac,
'recent_total_submission': recent_submission,
'recent_avg_ac': statistics.mean(recent_ac_list),
'recent_median_ac': statistics.median(recent_ac_list),
'recent_top_10_avg': statistics.mean(recent_ac_list[:min(10, len(recent_ac_list))]) if recent_ac_list else 0,
'recent_active_count': sum(1 for ac in recent_ac_list if ac > 0),
}
class_comparisons.append({
'class_name': class_name,
'user_count': user_count,
# 基础统计
'total_ac': int(total_ac),
'total_submission': int(total_submission),
'avg_ac': round(avg_ac, 2),
# 中位数和分位数
'median_ac': round(median_ac, 2),
'q1_ac': round(q1_ac, 2),
'q3_ac': round(q3_ac, 2),
'iqr': round(iqr, 2),
# 标准差
'std_dev': round(std_dev, 2),
# 分层统计
'top_10_avg': round(top_10_avg, 2),
'bottom_10_avg': round(bottom_10_avg, 2),
'top_25_avg': round(top_25_avg, 2),
'bottom_25_avg': round(bottom_25_avg, 2),
# 比率统计
'excellent_rate': round(excellent_rate, 2),
'pass_rate': round(pass_rate, 2),
'active_rate': round(active_rate, 2),
# 正确率
'ac_rate': round(total_ac / total_submission * 100, 2) if total_submission > 0 else 0,
# 时间段统计(如果有)
**recent_stats,
})
# 按总AC数排序
class_comparisons.sort(key=lambda x: (-x['total_ac'], x['total_submission']))
return self.success({
'comparisons': class_comparisons,
'has_time_range': bool(start_time and end_time)
})