add flowchart list

This commit is contained in:
2025-10-20 20:05:10 +08:00
parent 6465f8fab2
commit a103dd6b38
4 changed files with 156 additions and 52 deletions

View File

@@ -17,19 +17,19 @@ from utils.shortcuts import get_env
from account.models import User from account.models import User
from problem.models import Problem from problem.models import Problem
from submission.models import Submission, JudgeStatus from submission.models import Submission, JudgeStatus
from flowchart.models import FlowchartSubmission, FlowchartSubmissionStatus
from account.decorators import login_required from account.decorators import login_required
from ai.models import AIAnalysis from ai.models import AIAnalysis
CACHE_TIMEOUT = 300 CACHE_TIMEOUT = 300
DIFFICULTY_MAP = {"Low": "简单", "Mid": "中等", "High": "困难"} DIFFICULTY_MAP = {"Low": "简单", "Mid": "中等", "High": "困难"}
DEFAULT_CLASS_SIZE = 45 DEFAULT_CLASS_SIZE = 45
# 评级阈值配置:(百分位上限, 评级) # 评级阈值配置:(百分位上限, 评级)
GRADE_THRESHOLDS = [ GRADE_THRESHOLDS = [
(10, "S"), # 前10%: S级 - 卓越 (10, "S"), # 前10%: S级 - 卓越
(35, "A"), # 前35%: A级 - 优秀 (35, "A"), # 前35%: A级 - 优秀
(75, "B"), # 前75%: B级 - 良好 (75, "B"), # 前75%: B级 - 良好
(100, "C"), # 其余: C级 - 及格 (100, "C"), # 其余: C级 - 及格
] ]
@@ -51,41 +51,41 @@ def get_difficulty(difficulty):
def get_grade(rank, submission_count): def get_grade(rank, submission_count):
""" """
计算题目完成评级 计算题目完成评级
评级标准: 评级标准:
- S级前10%卓越水平10%的人) - S级前10%卓越水平10%的人)
- A级前35%优秀水平25%的人) - A级前35%优秀水平25%的人)
- B级前75%良好水平40%的人) - B级前75%良好水平40%的人)
- C级75%之后及格水平25%的人) - C级75%之后及格水平25%的人)
特殊规则: 特殊规则:
- 参与人数少于10人时S级降为A级A级降为B级避免因人少而评级虚高 - 参与人数少于10人时S级降为A级A级降为B级避免因人少而评级虚高
Args: Args:
rank: 用户排名1表示第一名 rank: 用户排名1表示第一名
submission_count: 总AC人数 submission_count: 总AC人数
Returns: Returns:
评级字符串 ("S", "A", "B", "C") 评级字符串 ("S", "A", "B", "C")
""" """
# 边界检查 # 边界检查
if not rank or rank <= 0 or submission_count <= 0: if not rank or rank <= 0 or submission_count <= 0:
return "C" return "C"
# 计算百分位0-100使用 (rank-1) 使第一名的百分位为0 # 计算百分位0-100使用 (rank-1) 使第一名的百分位为0
percentile = (rank - 1) / submission_count * 100 percentile = (rank - 1) / submission_count * 100
# 根据百分位确定基础评级 # 根据百分位确定基础评级
base_grade = "C" base_grade = "C"
for threshold, grade in GRADE_THRESHOLDS: for threshold, grade in GRADE_THRESHOLDS:
if percentile < threshold: if percentile < threshold:
base_grade = grade base_grade = grade
break break
# 小规模参与惩罚:人数太少时降低评级 # 小规模参与惩罚:人数太少时降低评级
if submission_count < SMALL_SCALE_PENALTY["threshold"]: if submission_count < SMALL_SCALE_PENALTY["threshold"]:
base_grade = SMALL_SCALE_PENALTY["downgrade"].get(base_grade, base_grade) base_grade = SMALL_SCALE_PENALTY["downgrade"].get(base_grade, base_grade)
return base_grade return base_grade
@@ -163,6 +163,7 @@ class AIDetailDataAPI(APIView):
"start": start, "start": start,
"end": end, "end": end,
"solved": [], "solved": [],
"flowcharts": [],
"grade": "", "grade": "",
"tags": {}, "tags": {},
"difficulty": {}, "difficulty": {},
@@ -179,9 +180,79 @@ class AIDetailDataAPI(APIView):
solved, contest_ids = self._build_solved_records( solved, contest_ids = self._build_solved_records(
user_first_ac, by_problem, problems, user.id user_first_ac, by_problem, problems, user.id
) )
# 查找 flowchart submissions
flowcharts_query = FlowchartSubmission.objects.filter(
user_id=user,
status=FlowchartSubmissionStatus.COMPLETED,
)
# 添加时间范围过滤
if start:
flowcharts_query = flowcharts_query.filter(create_time__gte=start)
if end:
flowcharts_query = flowcharts_query.filter(create_time__lte=end)
flowcharts = flowcharts_query.select_related("problem").only(
"id",
"create_time",
"ai_score",
"ai_grade",
"problem___id",
"problem__title",
)
# 按problem分组
problem_groups = defaultdict(list)
for flowchart in flowcharts:
problem_id = flowchart.problem._id
problem_groups[problem_id].append(flowchart)
flowcharts_data = []
for problem_id, submissions in problem_groups.items():
if not submissions:
continue
# 获取第一个提交的基本信息
first_submission = submissions[0]
# 计算统计数据
scores = [s.ai_score for s in submissions if s.ai_score is not None]
times = [s.create_time for s in submissions]
# 找到最高分和对应的等级
best_score = max(scores) if scores else 0
best_submission = next(
(s for s in submissions if s.ai_score == best_score), submissions[0]
)
best_grade = best_submission.ai_grade or ""
# 计算平均分
avg_score = sum(scores) / len(scores) if scores else 0
# 最新提交时间
latest_time = max(times) if times else first_submission.create_time
merged_item = {
"problem__id": problem_id,
"problem_title": first_submission.problem.title,
"submission_count": len(submissions),
"best_score": best_score,
"best_grade": best_grade,
"latest_submission_time": latest_time.isoformat() if latest_time else None,
"avg_score": round(avg_score, 0),
}
flowcharts_data.append(merged_item)
# 按最新提交时间排序
flowcharts_data.sort(
key=lambda x: x["latest_submission_time"] or "", reverse=True
)
result.update( result.update(
{ {
"solved": solved, "solved": solved,
"flowcharts": flowcharts_data,
"grade": self._calculate_average_grade(solved), "grade": self._calculate_average_grade(solved),
"tags": self._calculate_top_tags(problems.values()), "tags": self._calculate_top_tags(problems.values()),
"difficulty": self._calculate_difficulty_distribution( "difficulty": self._calculate_difficulty_distribution(
@@ -236,38 +307,38 @@ class AIDetailDataAPI(APIView):
def _calculate_average_grade(self, solved): def _calculate_average_grade(self, solved):
""" """
计算平均等级,使用加权平均方法 计算平均等级,使用加权平均方法
等级权重S=4, A=3, B=2, C=1 等级权重S=4, A=3, B=2, C=1
计算加权平均后,根据阈值确定最终等级 计算加权平均后,根据阈值确定最终等级
Args: Args:
solved: 已解决的题目列表每个包含grade字段 solved: 已解决的题目列表每个包含grade字段
Returns: Returns:
平均等级字符串 ("S", "A", "B", "C") 平均等级字符串 ("S", "A", "B", "C")
""" """
if not solved: if not solved:
return "" return ""
# 等级权重映射 # 等级权重映射
grade_weights = {"S": 4, "A": 3, "B": 2, "C": 1} grade_weights = {"S": 4, "A": 3, "B": 2, "C": 1}
# 计算加权总分 # 计算加权总分
total_weight = 0 total_weight = 0
total_score = 0 total_score = 0
for s in solved: for s in solved:
grade = s["grade"] grade = s["grade"]
if grade in grade_weights: if grade in grade_weights:
total_score += grade_weights[grade] total_score += grade_weights[grade]
total_weight += 1 total_weight += 1
if total_weight == 0: if total_weight == 0:
return "" return ""
# 计算平均权重 # 计算平均权重
average_weight = total_score / total_weight average_weight = total_score / total_weight
# 根据平均权重确定等级 # 根据平均权重确定等级
# S级: 3.5-4.0, A级: 2.5-3.5, B级: 1.5-2.5, C级: 1.0-1.5 # S级: 3.5-4.0, A级: 2.5-3.5, B级: 1.5-2.5, C级: 1.0-1.5
if average_weight >= 3.5: if average_weight >= 3.5:
@@ -395,28 +466,28 @@ class AIDurationDataAPI(APIView):
def _calculate_period_grade(self, user_first_ac, by_problem, user_id): def _calculate_period_grade(self, user_first_ac, by_problem, user_id):
""" """
计算时间段内的平均等级,使用加权平均方法 计算时间段内的平均等级,使用加权平均方法
等级权重S=4, A=3, B=2, C=1 等级权重S=4, A=3, B=2, C=1
计算加权平均后,根据阈值确定最终等级 计算加权平均后,根据阈值确定最终等级
Args: Args:
user_first_ac: 用户首次AC的提交记录 user_first_ac: 用户首次AC的提交记录
by_problem: 按题目分组的排名数据 by_problem: 按题目分组的排名数据
user_id: 用户ID user_id: 用户ID
Returns: Returns:
平均等级字符串 ("S", "A", "B", "C") 平均等级字符串 ("S", "A", "B", "C")
""" """
if not user_first_ac: if not user_first_ac:
return "" return ""
# 等级权重映射 # 等级权重映射
grade_weights = {"S": 4, "A": 3, "B": 2, "C": 1} grade_weights = {"S": 4, "A": 3, "B": 2, "C": 1}
# 计算加权总分 # 计算加权总分
total_weight = 0 total_weight = 0
total_score = 0 total_score = 0
for item in user_first_ac: for item in user_first_ac:
ranking_list = by_problem.get(item["problem_id"], []) ranking_list = by_problem.get(item["problem_id"], [])
rank = next( rank = next(
@@ -432,13 +503,13 @@ class AIDurationDataAPI(APIView):
if grade in grade_weights: if grade in grade_weights:
total_score += grade_weights[grade] total_score += grade_weights[grade]
total_weight += 1 total_weight += 1
if total_weight == 0: if total_weight == 0:
return "" return ""
# 计算平均权重 # 计算平均权重
average_weight = total_score / total_weight average_weight = total_score / total_weight
# 根据平均权重确定等级 # 根据平均权重确定等级
# S级: 3.5-4.0, A级: 2.5-3.5, B级: 1.5-2.5, C级: 1.0-1.5 # S级: 3.5-4.0, A级: 2.5-3.5, B级: 1.5-2.5, C级: 1.0-1.5
if average_weight >= 3.5: if average_weight >= 3.5:
@@ -572,9 +643,10 @@ class AIHeatmapDataAPI(APIView):
submission_count = submission_dict.get(day_date, 0) submission_count = submission_dict.get(day_date, 0)
heatmap_data.append( heatmap_data.append(
{ {
"timestamp": int(datetime.combine( "timestamp": int(
day_date, datetime.min.time() datetime.combine(day_date, datetime.min.time()).timestamp()
).timestamp() * 1000), * 1000
),
"value": submission_count, "value": submission_count,
} }
) )

View File

@@ -41,14 +41,15 @@ class FlowchartSubmissionListSerializer(serializers.ModelSerializer):
"""用于列表显示的简化序列化器""" """用于列表显示的简化序列化器"""
username = serializers.CharField(source="user.username") username = serializers.CharField(source="user.username")
problem = serializers.CharField(source="problem._id")
problem_title = serializers.CharField(source="problem.title") problem_title = serializers.CharField(source="problem.title")
class Meta: class Meta:
model = FlowchartSubmission model = FlowchartSubmission
fields = [ fields = [
"id", "id",
"username", "username",
"problem_title", "problem_title",
"problem",
"status", "status",
"create_time", "create_time",
"ai_score", "ai_score",
@@ -58,3 +59,33 @@ class FlowchartSubmissionListSerializer(serializers.ModelSerializer):
"processing_time", "processing_time",
"evaluation_time", "evaluation_time",
] ]
class FlowchartSubmissionSummarySerializer(serializers.ModelSerializer):
"""用于AI详情页面的极简序列化器只包含必要字段"""
problem_title = serializers.CharField(source="problem.title")
problem__id = serializers.CharField(source="problem._id")
class Meta:
model = FlowchartSubmission
fields = [
"id",
"problem__id",
"problem_title",
"ai_score",
"ai_grade",
"create_time",
]
class FlowchartSubmissionMergedSerializer(serializers.Serializer):
"""合并后的流程图提交序列化器"""
problem__id = serializers.CharField()
problem_title = serializers.CharField()
submission_count = serializers.IntegerField()
best_score = serializers.FloatField()
best_grade = serializers.CharField()
latest_submission_time = serializers.DateTimeField()
avg_score = serializers.FloatField()

View File

@@ -65,32 +65,32 @@ class FlowchartSubmissionAPI(APIView):
class FlowchartSubmissionListAPI(APIView): class FlowchartSubmissionListAPI(APIView):
@login_required
def get(self, request): def get(self, request):
"""获取流程图提交列表""" """获取流程图提交列表"""
user_id = request.GET.get("user_id") username = request.GET.get("username")
problem_id = request.GET.get("problem_id") problem_id = request.GET.get("problem_id")
offset = int(request.GET.get("offset", 0)) myself = request.GET.get("myself")
limit = int(request.GET.get("limit", 20))
queryset = FlowchartSubmission.objects.select_related("user", "problem") queryset = FlowchartSubmission.objects.select_related("user", "problem")
# 权限过滤
if not request.user.is_admin_role():
queryset = queryset.filter(user=request.user)
# 其他过滤条件
if user_id:
queryset = queryset.filter(user_id=user_id)
if problem_id: if problem_id:
queryset = queryset.filter(problem_id=problem_id) try:
problem = Problem.objects.get(
_id=problem_id, contest_id__isnull=True, visible=True
)
except Problem.DoesNotExist:
return self.error("Problem doesn't exist")
queryset = queryset.filter(problem=problem)
if myself and myself == "1":
queryset = queryset.filter(user=request.user)
if username:
queryset = queryset.filter(user__username__icontains=username)
total = queryset.count() data = self.paginate_data(request, queryset)
submissions = queryset[offset : offset + limit] data["results"] = FlowchartSubmissionListSerializer(
data["results"], many=True
serializer = FlowchartSubmissionListSerializer(submissions, many=True) ).data
return self.success(data)
return self.success({"results": serializer.data, "total": total})
class FlowchartSubmissionRetryAPI(APIView): class FlowchartSubmissionRetryAPI(APIView):

View File

@@ -34,6 +34,7 @@ class SubmissionSafeModelSerializer(serializers.ModelSerializer):
class SubmissionListSerializer(serializers.ModelSerializer): class SubmissionListSerializer(serializers.ModelSerializer):
problem = serializers.SlugRelatedField(read_only=True, slug_field="_id") problem = serializers.SlugRelatedField(read_only=True, slug_field="_id")
problem_title = serializers.CharField(source="problem.title")
show_link = serializers.SerializerMethodField() show_link = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):