This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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='权重'),
|
||||
),
|
||||
]
|
||||
@@ -149,6 +149,41 @@
|
||||
<!-- Loaded via JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评分细项(仅评委和嘉宾可见) -->
|
||||
<div id="scoreDetailsSection" style="display: none;" class="bg-gradient-to-br from-gray-50 to-blue-50 rounded-xl p-5 border border-gray-200 shadow-sm">
|
||||
<h4 class="text-lg font-bold text-gray-900 mb-4 flex items-center"><i class="fas fa-chart-bar mr-2 text-indigo-500"></i>评分明细</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="bg-white rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase">评委评分</span>
|
||||
<i class="fas fa-user-tie text-blue-400 text-sm"></i>
|
||||
</div>
|
||||
<span id="judgeScoreValue" class="text-2xl font-bold text-blue-600">--</span>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase">选手互评分</span>
|
||||
<i class="fas fa-users text-green-400 text-sm"></i>
|
||||
</div>
|
||||
<span id="peerScoreValue" class="text-2xl font-bold text-green-600">--</span>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase">AI评分</span>
|
||||
<i class="fas fa-robot text-purple-400 text-sm"></i>
|
||||
</div>
|
||||
<span id="aiScoreValue" class="text-2xl font-bold text-purple-600">--</span>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase">最终平均分</span>
|
||||
<i class="fas fa-star text-yellow-400 text-sm"></i>
|
||||
</div>
|
||||
<span id="finalScoreValue" class="text-2xl font-bold text-yellow-600">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Grading -->
|
||||
@@ -601,6 +636,18 @@ async function viewProject(id) {
|
||||
).join('') : '<div class="text-center text-gray-400 py-4 text-sm">暂无历史评语</div>';
|
||||
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 => `
|
||||
<div class="bg-white p-3 rounded-lg border border-gray-200" data-dimension-id="${d.id}" data-max-score="${d.max_score}" data-weight="${d.weight}">
|
||||
|
||||
Reference in New Issue
Block a user