forked from quant-speed-AI/Scoring-System
338 lines
14 KiB
Python
338 lines
14 KiB
Python
from django.db import models
|
||
from shop.models import WeChatUser
|
||
|
||
|
||
class HomePageConfig(models.Model):
|
||
"""首页配置"""
|
||
banner_image = models.ImageField(upload_to='homepage/', verbose_name="首页Banner图片", null=True, blank=True)
|
||
banner_image_url = models.URLField(verbose_name="Banner图片URL", null=True, blank=True, help_text="优先使用上传的图片")
|
||
|
||
main_title = models.CharField(max_length=200, default='"创赢未来"云南2026创业大赛', verbose_name="主标题")
|
||
|
||
carousel1_title = models.CharField(max_length=200, default='"创赢未来"云南2026创业大赛', verbose_name="轮播图1标题")
|
||
carousel2_title = models.CharField(max_length=200, default='"七彩云南创业福地"创业主题系列活动', verbose_name="轮播图2标题")
|
||
|
||
organizer = models.CharField(max_length=200, default='云南省人力资源和社会保障厅', verbose_name="主办单位")
|
||
undertaker = models.CharField(max_length=200, default='云南省就业局', 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="更新时间")
|
||
|
||
class Meta:
|
||
verbose_name = "首页配置"
|
||
verbose_name_plural = "首页配置"
|
||
|
||
def __str__(self):
|
||
return "首页配置"
|
||
|
||
def get_banner_url(self):
|
||
if self.banner_image:
|
||
return self.banner_image.url
|
||
return self.banner_image_url
|
||
|
||
|
||
class CarouselItem(models.Model):
|
||
"""轮播图项目"""
|
||
CAROUSEL_TYPE_CHOICES = (
|
||
('carousel1', '创业大赛轮播图'),
|
||
('carousel2', '创业活动轮播图'),
|
||
)
|
||
|
||
STATUS_CHOICES = (
|
||
('报名中', '报名中'),
|
||
('即将开始', '即将开始'),
|
||
('敬请期待', '敬请期待'),
|
||
('进行中', '进行中'),
|
||
)
|
||
|
||
carousel_type = models.CharField(max_length=20, choices=CAROUSEL_TYPE_CHOICES, default='carousel1', verbose_name="轮播图类型")
|
||
|
||
image = models.ImageField(upload_to='homepage/carousel/', verbose_name="轮播图片", null=True, blank=True)
|
||
image_url = models.CharField(max_length=500, verbose_name="图片URL", null=True, blank=True, help_text="可填写本地路径如 /carousel1.png 或完整URL,优先使用上方上传的图片")
|
||
|
||
title = models.CharField(max_length=100, verbose_name="标题")
|
||
subtitle = models.CharField(max_length=200, verbose_name="副标题")
|
||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='报名中', verbose_name="状态")
|
||
status_color = models.CharField(max_length=20, default='#52c41a', verbose_name="状态颜色")
|
||
|
||
date = models.CharField(max_length=100, verbose_name="日期")
|
||
location = models.CharField(max_length=100, verbose_name="地点")
|
||
|
||
order = models.IntegerField(default=0, 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="更新时间")
|
||
|
||
class Meta:
|
||
verbose_name = "轮播图项目"
|
||
verbose_name_plural = "轮播图管理"
|
||
ordering = ['order', 'id']
|
||
|
||
def __str__(self):
|
||
return f"{self.get_carousel_type_display()} - {self.title}"
|
||
|
||
def get_image_url(self):
|
||
if self.image:
|
||
return self.image.url
|
||
return self.image_url
|
||
|
||
|
||
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=6, decimal_places=4, default=1.0000, verbose_name="权重", help_text="例如 0.3000 表示 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(维度分数 × 维度权重)
|
||
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
|
||
|
||
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}"
|