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="允许选手互评") 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): """ 评分维度配置 """ 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=5, decimal_places=2, default=1.00, verbose_name="权重", help_text="例如 0.3 表示 30%") max_score = models.IntegerField(default=100, verbose_name="满分值") 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}" 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. 获取所有评委对该项目的打分 2. 按维度加权平均 这里简化处理: 总分 = (所有评委的总加权分之和) / 评委人数 其中每个评委对项目的打分 = sum(维度分 * 维度权重) """ # 获取所有评分 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) current_judge_total_score = 0 current_judge_total_weight = 0 for score in judge_scores: current_judge_total_score += score.score * score.dimension.weight current_judge_total_weight += score.dimension.weight if current_judge_total_weight > 0: judge_score = current_judge_total_score / current_judge_total_weight # 如果是百分制,这里算出来就是0-100 total_weighted_score += judge_score # 平均分 avg_score = total_weighted_score / len(judges) self.final_score = avg_score self.save() return avg_score 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}"