From 98db4d6f758c211052dd6b62dc04e5a832b03b28 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Fri, 20 Mar 2026 13:13:07 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/competition/admin.py | 11 +++- backend/competition/models.py | 114 ++++++++++++++++++++++++++++++---- 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/backend/competition/admin.py b/backend/competition/admin.py index 69a8937..798a5ff 100644 --- a/backend/competition/admin.py +++ b/backend/competition/admin.py @@ -7,7 +7,12 @@ class ScoreDimensionInline(admin.TabularInline): model = ScoreDimension extra = 1 tab = True - fields = ('name', 'description', 'weight', 'max_score', 'is_public', 'is_peer_review', 'order') + fields = ('name', 'description', 'weight', 'max_score', 'formula_type', 'formula', 'is_public', 'is_peer_review', 'order') + + @admin.display(description="算式预览") + def formula_preview(self, obj): + preview = obj.get_formula_preview() + return preview if preview else "-" class ProjectFileInline(admin.TabularInline): model = ProjectFile @@ -32,6 +37,10 @@ class CompetitionAdmin(ModelAdmin): ('时间和状态', { 'fields': ('start_time', 'end_time', 'status', 'project_visibility', 'allow_contestant_grading', 'is_active') }), + ('评分计算设置', { + 'fields': ('score_calculation_type', 'custom_score_formula'), + 'description': '配置得分计算方式:默认模式使用(维度分数×权重)求和后取评委平均;自定义算式使用下方公式直接计算最终得分。变量格式: dimension_维度ID,如 dimension_1, dimension_2' + }), ) actions = ['make_published', 'make_ended'] diff --git a/backend/competition/models.py b/backend/competition/models.py index aa16bd3..72aa391 100644 --- a/backend/competition/models.py +++ b/backend/competition/models.py @@ -37,6 +37,14 @@ class Competition(models.Model): allow_contestant_grading = models.BooleanField(default=False, verbose_name="允许选手互评") + SCORE_CALCULATION_CHOICES = ( + ('default', '默认加权平均'), + ('custom', '自定义算式'), + ) + + score_calculation_type = models.CharField(max_length=20, choices=SCORE_CALCULATION_CHOICES, default='default', verbose_name="得分计算方式") + custom_score_formula = models.CharField(max_length=1000, blank=True, verbose_name="自定义得分算式", help_text="如使用自定义算式,将使用此公式计算最终得分。变量格式: dimension_维度ID,如 dimension_1, dimension_2") + is_active = models.BooleanField(default=True, verbose_name="是否启用") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") @@ -88,12 +96,20 @@ class ScoreDimension(models.Model): """ 评分维度配置 """ + FORMULA_TYPE_CHOICES = ( + ('weight', '权重模式'), + ('formula', '自定义算式'), + ) + competition = models.ForeignKey(Competition, on_delete=models.CASCADE, related_name='score_dimensions', verbose_name="所属比赛") name = models.CharField(max_length=100, verbose_name="维度名称") description = models.TextField(verbose_name="维度说明", blank=True) weight = models.DecimalField(max_digits=6, decimal_places=4, default=1.0000, verbose_name="权重", help_text="例如 0.3000 表示 30%") max_score = models.IntegerField(default=100, verbose_name="满分值") + formula_type = models.CharField(max_length=20, choices=FORMULA_TYPE_CHOICES, default='weight', verbose_name="算式类型") + formula = models.CharField(max_length=500, blank=True, verbose_name="自定义算式", help_text="使用维度ID作为变量,如: dimension_1 * 0.3 + dimension_2 * 0.5 + dimension_3 * 0.2") + is_public = models.BooleanField(default=True, verbose_name="是否公开给评委", help_text="如果关闭,评委端将看不到此评分维度,通常用于AI自动评分") is_peer_review = models.BooleanField(default=False, verbose_name="是否用于选手互评", help_text="如果开启,此评分维度仅在选手互评时可见,评委和嘉宾看不到") @@ -107,6 +123,20 @@ class ScoreDimension(models.Model): def __str__(self): return f"{self.competition.title} - {self.name}" + + def get_formula_preview(self): + """ + 获取算式预览,显示维度名称而非ID + """ + if not self.formula or self.formula_type != 'formula': + return None + + dimension_map = {d.id: d.name for d in self.competition.score_dimensions.all()} + + result = self.formula + for dim_id, dim_name in dimension_map.items(): + result = result.replace(f'dimension_{dim_id}', f'[{dim_name}]') + return result class Project(models.Model): @@ -147,39 +177,97 @@ class Project(models.Model): def calculate_score(self): """ 计算项目得分 - 计算公式: + 支持两种模式: + 1. 默认加权平均:每个评委的得分 = sum(维度分数 × 维度权重),然后所有评委取平均 + 2. 自定义算式:使用比赛级别的 custom_score_formula 计算最终得分 + + 自定义算式变量格式: + - dimension_X: 第X个维度的平均分(所有评委对该维度的平均分) + - 也可以在算式中直接使用维度ID + """ + scores = self.scores.all() + if not scores.exists(): + self.final_score = 0 + self.save() + return 0 + + competition = self.competition + + if competition.score_calculation_type == 'custom' and competition.custom_score_formula: + return self._calculate_custom_score(scores, competition.custom_score_formula) + + return self._calculate_default_score(scores) + + def _calculate_default_score(self, scores): + """ + 默认加权平均模式 1. 获取所有评委对该项目的打分 2. 每个评委的得分 = sum(维度分数 × 维度权重) 3. 项目最终得分 = 所有评委得分的平均值 """ - # 获取所有评分 - scores = self.scores.all() - if not scores.exists(): - return 0 - - # 找出所有参与评分的评委 judges = set(score.judge for score in scores) if not judges: return 0 - + total_weighted_score = 0 for judge in judges: judge_score = 0 - # 获取该评委对该项目的所有维度打分 judge_scores = scores.filter(judge=judge) for score in judge_scores: - # 直接用原始分数乘以权重相加 - judge_score += score.score * score.dimension.weight + judge_score += float(score.score) * float(score.dimension.weight) total_weighted_score += judge_score - # 平均分 avg_score = total_weighted_score / len(judges) - self.final_score = avg_score + self.final_score = round(avg_score, 2) self.save() return avg_score + + def _calculate_custom_score(self, scores, formula): + """ + 自定义算式模式 + 使用比赛配置的自定义算式计算得分 + """ + dimension_scores = {} + + dimensions = self.competition.score_dimensions.all() + for dimension in dimensions: + dim_scores = scores.filter(dimension=dimension) + if dim_scores.exists(): + avg = sum(float(s.score) for s in dim_scores) / dim_scores.count() + dimension_scores[f'dimension_{dimension.id}'] = avg + + if not dimension_scores: + self.final_score = 0 + self.save() + return 0 + + try: + result = eval(formula, {"__builtins__": {}}, dimension_scores) + final_score = float(result) + self.final_score = round(final_score, 2) + self.save() + return self.final_score + except Exception as e: + print(f"算式计算错误: {e}, formula: {formula}, values: {dimension_scores}") + return self._calculate_default_score(scores) + + def calculate_judge_score(self, judge): + """ + 计算指定评委对该项目的得分 + 用于显示评委个人评分 + """ + scores = self.scores.filter(judge=judge) + if not scores.exists(): + return 0 + + total = 0 + for score in scores: + total += float(score.score) * float(score.dimension.weight) + + return round(total, 2) class ProjectFile(models.Model):