Files
market_page/backend/competition/models.py
jeremygan2021 98db4d6f75
All checks were successful
Deploy to Server / deploy (push) Successful in 28s
算法
2026-03-20 13:13:07 +08:00

349 lines
14 KiB
Python
Raw 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', '默认加权平均'),
('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="更新时间")
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. 自定义算式:使用比赛级别的 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. 项目最终得分 = 所有评委得分的平均值
"""
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_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):
"""
项目附件
"""
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}"