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}"