Files
jeremygan2021 8bc06b0423
All checks were successful
Deploy to Server / deploy (push) Successful in 18s
score
2026-03-20 15:34:09 +08:00

426 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import models
from shop.models import WeChatUser
class Competition(models.Model):
"""
比赛管理模型
"""
STATUS_CHOICES = (
('draft', '草稿'),
('published', '已发布'),
('registration', '报名中'),
('submission', '作品提交中'),
('judging', '评审中'),
('ended', '已结束'),
)
PROJECT_VISIBILITY_CHOICES = (
('public', '公开可见'),
('contestant', '选手及以上可见'),
('guest', '嘉宾及评委可见'),
('judge', '仅评委可见'),
)
title = models.CharField(max_length=200, verbose_name="比赛名称")
description = models.TextField(verbose_name="比赛简介")
rule_description = models.TextField(verbose_name="规则说明")
condition_description = models.TextField(verbose_name="参赛条件说明", blank=True)
cover_image = models.ImageField(upload_to='competitions/covers/', verbose_name="封面图", null=True, blank=True)
cover_image_url = models.URLField(verbose_name="封面图URL", null=True, blank=True, help_text="优先使用上传的图片")
start_time = models.DateTimeField(verbose_name="开始时间")
end_time = models.DateTimeField(verbose_name="结束时间")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name="状态")
project_visibility = models.CharField(max_length=20, choices=PROJECT_VISIBILITY_CHOICES, default='public', verbose_name="项目可见性")
allow_contestant_grading = models.BooleanField(default=False, verbose_name="允许选手互评")
SCORE_CALCULATION_CHOICES = (
('default', '默认加权平均'),
('formula', '使用评分公式'),
)
score_calculation_type = models.CharField(max_length=20, choices=SCORE_CALCULATION_CHOICES, default='default', verbose_name="得分计算方式")
active_formula = models.ForeignKey('ScoreFormula', on_delete=models.SET_NULL, null=True, blank=True, related_name='active_competitions', verbose_name="启用的评分公式")
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="更新时间")
def __str__(self):
return self.title
class Meta:
verbose_name = "比赛"
verbose_name_plural = "比赛管理"
ordering = ['-created_at']
class CompetitionEnrollment(models.Model):
"""
比赛人员报名/角色分配
"""
ROLE_CHOICES = (
('contestant', '选手'),
('judge', '评委'),
('guest', '嘉宾'),
)
STATUS_CHOICES = (
('pending', '待审核'),
('approved', '已通过'),
('rejected', '已拒绝'),
)
competition = models.ForeignKey(Competition, on_delete=models.CASCADE, related_name='enrollments', verbose_name="所属比赛")
user = models.ForeignKey(WeChatUser, on_delete=models.CASCADE, related_name='competitions', verbose_name="用户")
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='contestant', verbose_name="角色")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name="状态")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="申请时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "比赛人员"
verbose_name_plural = "人员管理"
unique_together = ('competition', 'user')
def __str__(self):
return f"{self.competition.title} - {self.user.nickname} ({self.get_role_display()})"
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="如果开启,此评分维度仅在选手互评时可见,评委和嘉宾看不到")
order = models.IntegerField(default=0, verbose_name="排序权重")
class Meta:
verbose_name = "评分维度"
verbose_name_plural = "评分维度配置"
ordering = ['order']
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):
"""
参赛项目/作品
"""
STATUS_CHOICES = (
('draft', '草稿'),
('submitted', '已提交'),
)
competition = models.ForeignKey(Competition, on_delete=models.CASCADE, related_name='projects', verbose_name="所属比赛")
contestant = models.ForeignKey(CompetitionEnrollment, on_delete=models.CASCADE, related_name='projects', verbose_name="参赛选手")
title = models.CharField(max_length=200, verbose_name="项目名称")
description = models.TextField(verbose_name="项目介绍")
team_info = models.TextField(verbose_name="团队介绍", blank=True)
cover_image = models.ImageField(upload_to='competitions/projects/covers/', verbose_name="项目封面", null=True, blank=True)
cover_image_url = models.URLField(verbose_name="项目封面URL", null=True, blank=True, help_text="优先使用上传的图片")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name="状态")
# 最终得分缓存,避免每次实时计算
final_score = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name="最终得分")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "参赛项目"
verbose_name_plural = "项目管理"
ordering = ['-final_score', '-created_at']
def __str__(self):
return self.title
def calculate_score(self):
"""
计算项目得分
支持两种模式:
1. 默认加权平均:每个评委的得分 = sum(维度分数 × 维度权重),然后所有评委取平均
2. 使用评分公式:使用比赛关联的评分公式计算最终得分
评分公式变量格式:
- dimension_X: 第X个维度的平均分所有评委对该维度的平均分
- 例如: (dimension_1 + dimension_2) / 2
"""
scores = self.scores.all()
if not scores.exists():
self.final_score = 0
self.save()
return 0
competition = self.competition
if competition.score_calculation_type == 'formula' and competition.active_formula:
return self._calculate_formula_score(scores, competition.active_formula)
return self._calculate_default_score(scores)
def _calculate_default_score(self, scores):
"""
默认加权平均模式
1. 获取所有评委对该项目的打分
2. 每个评委的得分 = sum(维度分数 × 维度权重)
3. 项目最终得分 = 所有评委得分的平均值
"""
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 += float(score.score) * float(score.dimension.weight)
total_weighted_score += judge_score
avg_score = total_weighted_score / len(judges)
self.final_score = round(avg_score, 2)
self.save()
return avg_score
def _calculate_formula_score(self, scores, formula_obj):
"""
公式配置模式(使用 ScoreFormula 模型)
使用公式配置中的公式计算得分
变量格式: dimension_X (X为维度ID)
注意:公式中的维度值已经是加权后的分数(原始分数 × 权重)
保存的评分是原始分数(不乘权重),显示时乘以权重
"""
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()
weighted_score = avg * float(dimension.weight)
dimension_scores[f'dimension_{dimension.id}'] = weighted_score
else:
dimension_scores[f'dimension_{dimension.id}'] = 0
if not dimension_scores:
self.final_score = 0
self.save()
return 0
formula = formula_obj.formula
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):
"""
项目附件
"""
FILE_TYPE_CHOICES = (
('ppt', 'PPT演示文稿'),
('pdf', 'PDF文档'),
('image', '图片'),
('video', '视频'),
('doc', '文档'),
('other', '其他'),
)
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='files', verbose_name="所属项目")
file_type = models.CharField(max_length=20, choices=FILE_TYPE_CHOICES, default='other', verbose_name="文件类型")
file = models.FileField(upload_to='competitions/projects/files/', verbose_name="文件", null=True, blank=True)
file_url = models.URLField(verbose_name="文件链接", null=True, blank=True, help_text="视频等大文件建议使用外部链接")
name = models.CharField(max_length=100, verbose_name="文件名称", blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间")
class Meta:
verbose_name = "项目附件"
verbose_name_plural = "附件管理"
def __str__(self):
return self.name or f"{self.get_file_type_display()}"
class Score(models.Model):
"""
评委打分
"""
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='scores', verbose_name="所属项目")
judge = models.ForeignKey(CompetitionEnrollment, on_delete=models.CASCADE, related_name='given_scores', verbose_name="评委")
dimension = models.ForeignKey(ScoreDimension, on_delete=models.CASCADE, verbose_name="评分维度")
score = models.DecimalField(max_digits=5, decimal_places=1, verbose_name="得分")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="打分时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "评分记录"
verbose_name_plural = "评分记录"
unique_together = ('project', 'judge', 'dimension')
def __str__(self):
return f"{self.judge.user.nickname} -> {self.project.title}: {self.score}"
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# 触发重新计算分数
self.project.calculate_score()
class Comment(models.Model):
"""
评委评语
"""
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='comments', verbose_name="所属项目")
judge = models.ForeignKey(CompetitionEnrollment, on_delete=models.CASCADE, related_name='given_comments', verbose_name="评委")
content = models.TextField(verbose_name="评语内容")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="评论时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "评委评语"
verbose_name_plural = "评语管理"
def __str__(self):
return f"{self.judge.user.nickname} -> {self.project.title}"
class ScoreFormula(models.Model):
"""
评分公式配置
用于可视化配置得分计算公式
"""
competition = models.ForeignKey(Competition, on_delete=models.CASCADE, related_name='score_formulas', verbose_name="所属比赛")
name = models.CharField(max_length=100, verbose_name="公式名称", help_text="用于标识这个公式,方便管理")
description = models.TextField(verbose_name="公式说明", blank=True)
formula = models.TextField(verbose_name="计算公式", help_text="使用维度名称作为变量,支持四则运算和函数")
is_active = models.BooleanField(default=True, verbose_name="是否启用")
is_default = models.BooleanField(default=False, verbose_name="是否设为默认公式")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
verbose_name = "评分公式"
verbose_name_plural = "评分公式配置"
ordering = ['-is_default', '-created_at']
def __str__(self):
return f"{self.competition.title} - {self.name}"
def get_formula_preview(self):
"""
获取公式预览,将维度变量替换为维度名称
"""
if not self.formula:
return ""
dimension_map = {f'd["{d.name}"]': f'[{d.name}]' for d in self.competition.score_dimensions.all()}
dimension_map.update({f"d['{d.name}']": f'[{d.name}]' for d in self.competition.score_dimensions.all()})
dimension_map.update({f'dimension_{d.id}': f'[{d.name}]' for d in self.competition.score_dimensions.all()})
result = self.formula
for old, new in dimension_map.items():
result = result.replace(old, new)
return result
def generate_python_code(self):
"""
生成可执行的 Python 代码
"""
if not self.formula:
return ""
dimension_names = [d.name for d in self.competition.score_dimensions.all()]
code_lines = [
"def calculate_score(d):",
" '''",
f" 计算公式: {self.name}",
" 参数 d: 字典,键为维度名称,值为该维度的平均分",
" '''",
]
for name in dimension_names:
code_lines.append(f" {name} = d.get('{name}', 0)")
code_lines.append("")
code_lines.append(f" return {self.formula}")
return "\n".join(code_lines)