比赛
All checks were successful
Deploy to Server / deploy (push) Successful in 7m23s

This commit is contained in:
jeremygan2021
2026-03-10 10:37:56 +08:00
parent 29e18e1288
commit 00389e0709
7 changed files with 780 additions and 4 deletions

View File

@@ -1,3 +1,69 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment
# Register your models here.
class ScoreDimensionInline(admin.TabularInline):
model = ScoreDimension
extra = 1
tab = True
class ProjectFileInline(admin.TabularInline):
model = ProjectFile
extra = 0
tab = True
@admin.register(Competition)
class CompetitionAdmin(ModelAdmin):
list_display = ['title', 'status', 'start_time', 'end_time', 'is_active', 'created_at']
list_filter = ['status', 'is_active']
search_fields = ['title', 'description']
inlines = [ScoreDimensionInline]
actions = ['make_published', 'make_ended']
def make_published(self, request, queryset):
queryset.update(status='published')
make_published.short_description = "发布选中比赛"
def make_ended(self, request, queryset):
queryset.update(status='ended')
make_ended.short_description = "结束选中比赛"
@admin.register(CompetitionEnrollment)
class CompetitionEnrollmentAdmin(ModelAdmin):
list_display = ['competition', 'user', 'role', 'status', 'created_at']
list_filter = ['competition', 'role', 'status']
search_fields = ['user__nickname', 'competition__title']
actions = ['approve_enrollment', 'reject_enrollment']
def approve_enrollment(self, request, queryset):
queryset.update(status='approved')
approve_enrollment.short_description = "通过审核"
def reject_enrollment(self, request, queryset):
queryset.update(status='rejected')
reject_enrollment.short_description = "拒绝申请"
@admin.register(Project)
class ProjectAdmin(ModelAdmin):
list_display = ['title', 'competition', 'contestant', 'status', 'final_score', 'created_at']
list_filter = ['competition', 'status']
search_fields = ['title', 'contestant__user__nickname']
inlines = [ProjectFileInline]
readonly_fields = ['final_score']
@admin.register(Score)
class ScoreAdmin(ModelAdmin):
list_display = ['project', 'judge', 'dimension', 'score', 'created_at']
list_filter = ['project__competition', 'dimension']
search_fields = ['project__title', 'judge__user__nickname']
@admin.register(Comment)
class CommentAdmin(ModelAdmin):
list_display = ['project', 'judge', 'content_preview', 'created_at']
list_filter = ['project__competition']
search_fields = ['project__title', 'judge__user__nickname', 'content']
def content_preview(self, obj):
return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
content_preview.short_description = "评语内容"

View File

@@ -0,0 +1,141 @@
# Generated by Django 6.0.1 on 2026-03-10 02:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('shop', '0039_vccourse_video_embed_code'),
]
operations = [
migrations.CreateModel(
name='Competition',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200, verbose_name='比赛名称')),
('description', models.TextField(verbose_name='比赛简介')),
('rule_description', models.TextField(verbose_name='规则说明')),
('condition_description', models.TextField(blank=True, verbose_name='参赛条件说明')),
('cover_image', models.ImageField(blank=True, null=True, upload_to='competitions/covers/', verbose_name='封面图')),
('start_time', models.DateTimeField(verbose_name='开始时间')),
('end_time', models.DateTimeField(verbose_name='结束时间')),
('status', models.CharField(choices=[('draft', '草稿'), ('published', '已发布'), ('registration', '报名中'), ('submission', '作品提交中'), ('judging', '评审中'), ('ended', '已结束')], default='draft', max_length=20, 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='更新时间')),
],
options={
'verbose_name': '比赛',
'verbose_name_plural': '比赛管理',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='CompetitionEnrollment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('contestant', '选手'), ('judge', '评委'), ('guest', '嘉宾')], default='contestant', max_length=20, verbose_name='角色')),
('status', models.CharField(choices=[('pending', '待审核'), ('approved', '已通过'), ('rejected', '已拒绝')], default='pending', max_length=20, verbose_name='状态')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='申请时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='enrollments', to='competition.competition', verbose_name='所属比赛')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competitions', to='shop.wechatuser', verbose_name='用户')),
],
options={
'verbose_name': '比赛人员',
'verbose_name_plural': '人员管理',
'unique_together': {('competition', 'user')},
},
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200, verbose_name='项目名称')),
('description', models.TextField(verbose_name='项目介绍')),
('team_info', models.TextField(blank=True, verbose_name='团队介绍')),
('cover_image', models.ImageField(blank=True, null=True, upload_to='competitions/projects/covers/', verbose_name='项目封面')),
('status', models.CharField(choices=[('draft', '草稿'), ('submitted', '已提交')], default='draft', max_length=20, verbose_name='状态')),
('final_score', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='最终得分')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='competition.competition', verbose_name='所属比赛')),
('contestant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='competition.competitionenrollment', verbose_name='参赛选手')),
],
options={
'verbose_name': '参赛项目',
'verbose_name_plural': '项目管理',
'ordering': ['-final_score', '-created_at'],
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(verbose_name='评语内容')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='评论时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('judge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='given_comments', to='competition.competitionenrollment', verbose_name='评委')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='competition.project', verbose_name='所属项目')),
],
options={
'verbose_name': '评委评语',
'verbose_name_plural': '评语管理',
},
),
migrations.CreateModel(
name='ProjectFile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file_type', models.CharField(choices=[('ppt', 'PPT演示文稿'), ('pdf', 'PDF文档'), ('image', '图片'), ('video', '视频'), ('doc', '文档'), ('other', '其他')], default='other', max_length=20, verbose_name='文件类型')),
('file', models.FileField(blank=True, null=True, upload_to='competitions/projects/files/', verbose_name='文件')),
('file_url', models.URLField(blank=True, help_text='视频等大文件建议使用外部链接', null=True, verbose_name='文件链接')),
('name', models.CharField(blank=True, max_length=100, verbose_name='文件名称')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='上传时间')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='competition.project', verbose_name='所属项目')),
],
options={
'verbose_name': '项目附件',
'verbose_name_plural': '附件管理',
},
),
migrations.CreateModel(
name='ScoreDimension',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='维度名称')),
('description', models.TextField(blank=True, verbose_name='维度说明')),
('weight', models.DecimalField(decimal_places=2, default=1.0, help_text='例如 0.3 表示 30%', max_digits=5, verbose_name='权重')),
('max_score', models.IntegerField(default=100, verbose_name='满分值')),
('order', models.IntegerField(default=0, verbose_name='排序权重')),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='score_dimensions', to='competition.competition', verbose_name='所属比赛')),
],
options={
'verbose_name': '评分维度',
'verbose_name_plural': '评分维度配置',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='Score',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('score', models.DecimalField(decimal_places=1, max_digits=5, verbose_name='得分')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='打分时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('judge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='given_scores', to='competition.competitionenrollment', verbose_name='评委')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scores', to='competition.project', verbose_name='所属项目')),
('dimension', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.scoredimension', verbose_name='评分维度')),
],
options={
'verbose_name': '评分记录',
'verbose_name_plural': '评分记录',
'unique_together': {('project', 'judge', 'dimension')},
},
),
]

View File

@@ -1,3 +1,253 @@
from django.db import models
from shop.models import WeChatUser
# Create your models here.
class Competition(models.Model):
"""
比赛管理模型
"""
STATUS_CHOICES = (
('draft', '草稿'),
('published', '已发布'),
('registration', '报名中'),
('submission', '作品提交中'),
('judging', '评审中'),
('ended', '已结束'),
)
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)
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="状态")
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="满分值")
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)
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}"

View File

@@ -0,0 +1,72 @@
from rest_framework import serializers
from .models import Competition, CompetitionEnrollment, ScoreDimension, Project, ProjectFile, Score, Comment
from shop.serializers import WeChatUserSerializer
class ScoreDimensionSerializer(serializers.ModelSerializer):
class Meta:
model = ScoreDimension
fields = ['id', 'name', 'description', 'weight', 'max_score', 'order']
class CompetitionSerializer(serializers.ModelSerializer):
score_dimensions = ScoreDimensionSerializer(many=True, read_only=True)
class Meta:
model = Competition
fields = ['id', 'title', 'description', 'rule_description', 'condition_description',
'cover_image', 'start_time', 'end_time', 'status', 'is_active',
'score_dimensions', 'created_at']
class CompetitionEnrollmentSerializer(serializers.ModelSerializer):
user = WeChatUserSerializer(read_only=True)
class Meta:
model = CompetitionEnrollment
fields = ['id', 'competition', 'user', 'role', 'status', 'created_at']
read_only_fields = ['status']
class ProjectFileSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectFile
fields = ['id', 'project', 'file_type', 'file', 'file_url', 'name', 'created_at']
def validate_file(self, value):
if not value:
return value
# 50MB limit
limit_mb = 50
if value.size > limit_mb * 1024 * 1024:
raise serializers.ValidationError(f"文件大小不能超过 {limit_mb}MB")
return value
class ProjectSerializer(serializers.ModelSerializer):
files = ProjectFileSerializer(many=True, read_only=True)
contestant_info = serializers.SerializerMethodField()
class Meta:
model = Project
fields = ['id', 'competition', 'contestant', 'title', 'description', 'team_info',
'cover_image', 'status', 'final_score', 'files', 'contestant_info', 'created_at']
read_only_fields = ['final_score', 'contestant']
def get_contestant_info(self, obj):
return {
"nickname": obj.contestant.user.nickname,
"avatar_url": obj.contestant.user.avatar_url
}
class ScoreSerializer(serializers.ModelSerializer):
judge_name = serializers.CharField(source='judge.user.nickname', read_only=True)
dimension_name = serializers.CharField(source='dimension.name', read_only=True)
class Meta:
model = Score
fields = ['id', 'project', 'judge', 'dimension', 'score', 'judge_name', 'dimension_name', 'created_at']
read_only_fields = ['judge']
class CommentSerializer(serializers.ModelSerializer):
judge_name = serializers.CharField(source='judge.user.nickname', read_only=True)
class Meta:
model = Comment
fields = ['id', 'project', 'judge', 'content', 'judge_name', 'created_at']
read_only_fields = ['judge']

View File

@@ -0,0 +1,17 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
CompetitionViewSet, ProjectViewSet, ProjectFileViewSet,
ScoreViewSet, CommentViewSet
)
router = DefaultRouter()
router.register(r'competitions', CompetitionViewSet)
router.register(r'projects', ProjectViewSet, basename='project')
router.register(r'files', ProjectFileViewSet, basename='projectfile')
router.register(r'scores', ScoreViewSet, basename='score')
router.register(r'comments', CommentViewSet, basename='comment')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -1,3 +1,232 @@
from django.shortcuts import render
from rest_framework import viewsets, permissions, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Q
from shop.utils import get_current_wechat_user
from .models import Competition, CompetitionEnrollment, Project, ProjectFile, Score, Comment, ScoreDimension
from .serializers import (
CompetitionSerializer, CompetitionEnrollmentSerializer,
ProjectSerializer, ProjectFileSerializer,
ScoreSerializer, CommentSerializer, ScoreDimensionSerializer
)
# Create your views here.
class CompetitionViewSet(viewsets.ReadOnlyModelViewSet):
"""
比赛视图集
"""
queryset = Competition.objects.filter(is_active=True).order_by('-created_at')
serializer_class = CompetitionSerializer
permission_classes = [permissions.AllowAny]
filter_backends = [filters.SearchFilter]
search_fields = ['title', 'description']
@action(detail=True, methods=['post'], permission_classes=[permissions.AllowAny])
def enroll(self, request, pk=None):
"""
报名参加比赛
"""
competition = self.get_object()
user = get_current_wechat_user(request)
if not user:
return Response({"detail": "未登录"}, status=status.HTTP_401_UNAUTHORIZED)
role = request.data.get('role', 'contestant')
# 检查是否已报名
if CompetitionEnrollment.objects.filter(competition=competition, user=user).exists():
return Response({"detail": "您已报名该比赛"}, status=status.HTTP_400_BAD_REQUEST)
enrollment = CompetitionEnrollment.objects.create(
competition=competition,
user=user,
role=role,
status='pending' # 默认待审核
)
return Response(CompetitionEnrollmentSerializer(enrollment).data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=['get'])
def my_enrollment(self, request, pk=None):
"""
获取我的报名信息
"""
competition = self.get_object()
user = get_current_wechat_user(request)
if not user:
return Response({"detail": "未登录"}, status=status.HTTP_401_UNAUTHORIZED)
try:
enrollment = CompetitionEnrollment.objects.get(competition=competition, user=user)
return Response(CompetitionEnrollmentSerializer(enrollment).data)
except CompetitionEnrollment.DoesNotExist:
return Response({"detail": "未报名"}, status=status.HTTP_404_NOT_FOUND)
class ProjectViewSet(viewsets.ModelViewSet):
"""
参赛项目视图集
"""
serializer_class = ProjectSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
queryset = Project.objects.all()
competition_id = self.request.query_params.get('competition')
if competition_id:
queryset = queryset.filter(competition_id=competition_id)
# 如果是普通用户,只能看到已提交的项目,或者自己草稿的项目
user = get_current_wechat_user(self.request)
if user:
# 查找用户在这个比赛中的角色
# 如果是评委,可以看到所有项目(包括草稿吗?通常评委只看提交的)
# 这里简化评委看所有submitted用户看所有submitted + 自己的draft
# 找到用户参与的所有比赛角色
enrollments = CompetitionEnrollment.objects.filter(user=user)
judge_competitions = enrollments.filter(role='judge').values_list('competition_id', flat=True)
# 基本查询:所有已提交的项目
q = Q(status='submitted')
# 加上自己创建的项目 (即使是draft)
q |= Q(contestant__user=user)
# 加上自己是评委的比赛的所有项目 (通常评委只看submitted但如果需要预审可以看draft这里假设只看submitted)
# q |= Q(competition__in=judge_competitions)
queryset = queryset.filter(q)
else:
# 未登录用户只能看已提交
queryset = queryset.filter(status='submitted')
return queryset.order_by('-final_score', '-created_at')
def perform_create(self, serializer):
user = get_current_wechat_user(self.request)
if not user:
raise serializers.ValidationError("请先登录")
competition = serializer.validated_data['competition']
# 检查是否有参赛资格
try:
enrollment = CompetitionEnrollment.objects.get(
competition=competition,
user=user,
role='contestant',
status='approved'
)
except CompetitionEnrollment.DoesNotExist:
raise serializers.ValidationError("您没有参赛资格或审核未通过")
serializer.save(contestant=enrollment)
@action(detail=True, methods=['post'])
def submit(self, request, pk=None):
"""
提交项目(从草稿转为已提交)
"""
project = self.get_object()
user = get_current_wechat_user(request)
if project.contestant.user != user:
return Response({"detail": "无权操作"}, status=status.HTTP_403_FORBIDDEN)
project.status = 'submitted'
project.save()
return Response({"status": "submitted"})
class ProjectFileViewSet(viewsets.ModelViewSet):
"""
项目附件管理
"""
serializer_class = ProjectFileSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
return ProjectFile.objects.all()
def perform_create(self, serializer):
# 简单权限控制:只有项目拥有者可以上传
project = serializer.validated_data['project']
user = get_current_wechat_user(self.request)
if not user or project.contestant.user != user:
raise serializers.ValidationError("无权上传文件")
serializer.save()
class ScoreViewSet(viewsets.ModelViewSet):
"""
评分管理
"""
serializer_class = ScoreSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
project_id = self.request.query_params.get('project')
if project_id:
return Score.objects.filter(project_id=project_id)
return Score.objects.all()
def perform_create(self, serializer):
user = get_current_wechat_user(self.request)
if not user:
raise serializers.ValidationError("请先登录")
project = serializer.validated_data['project']
# 检查是否是评委
try:
enrollment = CompetitionEnrollment.objects.get(
competition=project.competition,
user=user,
role='judge',
status='approved'
)
except CompetitionEnrollment.DoesNotExist:
raise serializers.ValidationError("您不是该比赛的评委")
# 检查是否重复打分
dimension = serializer.validated_data['dimension']
if Score.objects.filter(project=project, judge=enrollment, dimension=dimension).exists():
raise serializers.ValidationError("您已对该维度打分")
serializer.save(judge=enrollment)
class CommentViewSet(viewsets.ModelViewSet):
"""
评语管理
"""
serializer_class = CommentSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
project_id = self.request.query_params.get('project')
if project_id:
return Comment.objects.filter(project_id=project_id)
return Comment.objects.all()
def perform_create(self, serializer):
user = get_current_wechat_user(self.request)
if not user:
raise serializers.ValidationError("请先登录")
project = serializer.validated_data['project']
# 检查是否是评委
try:
enrollment = CompetitionEnrollment.objects.get(
competition=project.competition,
user=user,
role='judge',
status='approved'
)
except CompetitionEnrollment.DoesNotExist:
raise serializers.ValidationError("您不是该比赛的评委")
serializer.save(judge=enrollment)

View File

@@ -8,6 +8,7 @@ urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('shop.urls')),
path('api/community/', include('community.urls')),
path('api/competition/', include('competition.urls')),
# Swagger文档路由
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),