diff --git a/backend/competition/judge_views.py b/backend/competition/judge_views.py index 6bf03f6..e70d661 100644 --- a/backend/competition/judge_views.py +++ b/backend/competition/judge_views.py @@ -18,7 +18,7 @@ import uuid from .models import Competition, CompetitionEnrollment, Project, Score, ScoreDimension, Comment, ProjectFile from shop.models import WeChatUser from shop.sms_utils import send_sms -from ai_services.models import TranscriptionTask +from ai_services.models import TranscriptionTask, AIEvaluation from ai_services.services import AliyunTingwuService logger = logging.getLogger(__name__) @@ -332,6 +332,81 @@ def project_detail_api(request, project_id): latest_task_any = TranscriptionTask.objects.filter(project=project).order_by('-created_at').first() audio_url = latest_task_any.file_url if latest_task_any else None + # 计算各类评分(仅对评委和嘉宾可见) + judge_score_avg = None + peer_score_avg = None + ai_score_avg = None + final_score = float(project.final_score) if project.final_score else 0 + + if role in ['judge', 'guest']: + # 评委评分:所有评委的平均分(不包括选手互评维度) + judge_dimensions = ScoreDimension.objects.filter( + competition=project.competition, + is_public=True, + is_peer_review=False + ) + judge_enrollments = CompetitionEnrollment.objects.filter( + competition=project.competition, + role='judge' + ) + + if judge_dimensions.exists() and judge_enrollments.exists(): + judge_total = 0 + judge_count = 0 + for judge_enrollment in judge_enrollments: + judge_project_scores = Score.objects.filter( + project=project, + judge=judge_enrollment, + dimension__in=judge_dimensions + ) + if judge_project_scores.exists(): + judge_score = sum( + float(s.score) * float(s.dimension.weight) + for s in judge_project_scores + ) + judge_total += judge_score + judge_count += 1 + if judge_count > 0: + judge_score_avg = round(judge_total / judge_count, 2) + + # 选手互评分:所有选手的平均分(仅互评维度) + peer_dimensions = ScoreDimension.objects.filter( + competition=project.competition, + is_peer_review=True + ) + peer_enrollments = CompetitionEnrollment.objects.filter( + competition=project.competition, + role='contestant' + ) + + if peer_dimensions.exists() and peer_enrollments.exists(): + peer_total = 0 + peer_count = 0 + for peer_enrollment in peer_enrollments: + peer_project_scores = Score.objects.filter( + project=project, + judge=peer_enrollment, + dimension__in=peer_dimensions + ) + if peer_project_scores.exists(): + peer_score = sum( + float(s.score) * float(s.dimension.weight) + for s in peer_project_scores + ) + peer_total += peer_score + peer_count += 1 + if peer_count > 0: + peer_score_avg = round(peer_total / peer_count, 2) + + # AI评分:来自AIEvaluation的平均分 + ai_evaluations = AIEvaluation.objects.filter( + task__project=project, + task__status='COMPLETED' + ) + ai_scores = [e.score for e in ai_evaluations if e.score is not None] + if ai_scores: + ai_score_avg = round(sum(ai_scores) / len(ai_scores), 2) + data = { 'id': project.id, 'title': project.title, @@ -342,7 +417,14 @@ def project_detail_api(request, project_id): 'current_comment': current_comment, 'ai_result': ai_data, 'audio_url': audio_url, - 'can_grade': role == 'judge' or (role == 'contestant' and project.contestant.user != user) # Contestant can grade others if allowed + 'can_grade': role == 'judge' or (role == 'contestant' and project.contestant.user != user), + # 评分细项(仅评委和嘉宾可见) + 'score_details': { + 'judge_score': judge_score_avg, + 'peer_score': peer_score_avg, + 'ai_score': ai_score_avg, + 'final_score': final_score + } if role in ['judge', 'guest'] else None } # Specifically for guest: can_grade is False diff --git a/backend/competition/migrations/0007_competition_custom_score_formula_and_more.py b/backend/competition/migrations/0007_competition_custom_score_formula_and_more.py new file mode 100644 index 0000000..f7f1192 --- /dev/null +++ b/backend/competition/migrations/0007_competition_custom_score_formula_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 6.0.1 on 2026-03-20 05:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('competition', '0006_add_peer_review_field'), + ] + + operations = [ + migrations.AddField( + model_name='competition', + name='custom_score_formula', + field=models.CharField(blank=True, help_text='如使用自定义算式,将使用此公式计算最终得分。变量格式: dimension_维度ID,如 dimension_1, dimension_2', max_length=1000, verbose_name='自定义得分算式'), + ), + migrations.AddField( + model_name='competition', + name='score_calculation_type', + field=models.CharField(choices=[('default', '默认加权平均'), ('custom', '自定义算式')], default='default', max_length=20, verbose_name='得分计算方式'), + ), + migrations.AddField( + model_name='scoredimension', + name='formula', + field=models.CharField(blank=True, help_text='使用维度ID作为变量,如: dimension_1 * 0.3 + dimension_2 * 0.5 + dimension_3 * 0.2', max_length=500, verbose_name='自定义算式'), + ), + migrations.AddField( + model_name='scoredimension', + name='formula_type', + field=models.CharField(choices=[('weight', '权重模式'), ('formula', '自定义算式')], default='weight', max_length=20, verbose_name='算式类型'), + ), + migrations.AlterField( + model_name='scoredimension', + name='weight', + field=models.DecimalField(decimal_places=4, default=1.0, help_text='例如 0.3000 表示 30%', max_digits=6, verbose_name='权重'), + ), + ] diff --git a/backend/competition/templates/judge/dashboard.html b/backend/competition/templates/judge/dashboard.html index 9841940..1f46812 100644 --- a/backend/competition/templates/judge/dashboard.html +++ b/backend/competition/templates/judge/dashboard.html @@ -149,6 +149,41 @@ + + + @@ -601,6 +636,18 @@ async function viewProject(id) { ).join('') : '
暂无历史评语
'; document.getElementById('modalHistoryComments').innerHTML = historyHtml; + // 渲染评分细项(仅评委和嘉宾可见) + const scoreDetailsSection = document.getElementById('scoreDetailsSection'); + if (data.score_details) { + scoreDetailsSection.style.display = 'block'; + document.getElementById('judgeScoreValue').innerText = data.score_details.judge_score !== null ? data.score_details.judge_score : '--'; + document.getElementById('peerScoreValue').innerText = data.score_details.peer_score !== null ? data.score_details.peer_score : '--'; + document.getElementById('aiScoreValue').innerText = data.score_details.ai_score !== null ? data.score_details.ai_score : '--'; + document.getElementById('finalScoreValue').innerText = data.score_details.final_score !== null ? data.score_details.final_score : '--'; + } else { + scoreDetailsSection.style.display = 'none'; + } + // Render Score Inputs const dimensionsHtml = data.dimensions.map(d => `