@@ -0,0 +1,194 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Question(models.Model):
|
||||
class Track(models.TextChoices):
|
||||
BEGINNER = "beginner", "入门"
|
||||
STANDARD = "standard", "标准"
|
||||
ADVANCED = "advanced", "进阶"
|
||||
OPEN = "open", "Open"
|
||||
|
||||
slug = models.SlugField(unique=True)
|
||||
track = models.CharField(max_length=16, choices=Track.choices)
|
||||
tags = models.JSONField(default=list, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.slug
|
||||
|
||||
|
||||
class QuestionVersion(models.Model):
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="versions")
|
||||
version = models.PositiveIntegerField()
|
||||
prompt = models.TextField()
|
||||
answer = models.CharField(max_length=200)
|
||||
explanation = models.TextField(blank=True)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=("question", "version"), name="unique_question_version")
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.question.slug} v{self.version}"
|
||||
|
||||
|
||||
class Contest(models.Model):
|
||||
class Kind(models.TextChoices):
|
||||
REALTIME = "realtime", "实时 1v1"
|
||||
DAILY = "daily", "今日挑战"
|
||||
PRACTICE = "practice", "单人闯关"
|
||||
WEEKLY = "weekly", "主题周赛"
|
||||
|
||||
class Status(models.TextChoices):
|
||||
DRAFT = "draft", "草稿"
|
||||
PUBLISHED = "published", "已发布"
|
||||
CLOSED = "closed", "已结束"
|
||||
|
||||
slug = models.SlugField(unique=True)
|
||||
title = models.CharField(max_length=120)
|
||||
kind = models.CharField(max_length=16, choices=Kind.choices)
|
||||
track = models.CharField(max_length=16, choices=Question.Track.choices)
|
||||
status = models.CharField(max_length=16, choices=Status.choices, default=Status.DRAFT)
|
||||
duration_seconds = models.PositiveIntegerField(default=60)
|
||||
starts_at = models.DateTimeField(null=True, blank=True)
|
||||
ends_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class ContestQuestion(models.Model):
|
||||
contest = models.ForeignKey(Contest, on_delete=models.CASCADE, related_name="contest_questions")
|
||||
question_version = models.ForeignKey(QuestionVersion, on_delete=models.PROTECT)
|
||||
order = models.PositiveIntegerField()
|
||||
points = models.PositiveIntegerField(default=100)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=("contest", "order"), name="unique_contest_question_order")
|
||||
]
|
||||
ordering = ["order"]
|
||||
|
||||
|
||||
class RealtimeMatch(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
WAITING = "waiting", "等待对手"
|
||||
ACTIVE = "active", "进行中"
|
||||
COMPLETED = "completed", "已完成"
|
||||
CANCELLED = "cancelled", "已取消"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
contest = models.ForeignKey(Contest, on_delete=models.PROTECT)
|
||||
player_one = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="matches_as_player_one"
|
||||
)
|
||||
player_two = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="matches_as_player_two",
|
||||
)
|
||||
status = models.CharField(max_length=16, choices=Status.choices, default=Status.WAITING)
|
||||
player_one_rating = models.PositiveIntegerField()
|
||||
player_two_rating = models.PositiveIntegerField(null=True, blank=True)
|
||||
winner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="won_matches",
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=("contest", "status", "player_one_rating", "created_at"),
|
||||
name="matchmaking_lookup_idx",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class ContestAttempt(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
ACTIVE = "active", "进行中"
|
||||
SUBMITTED = "submitted", "已提交"
|
||||
EXPIRED = "expired", "已超时"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
contest = models.ForeignKey(Contest, on_delete=models.PROTECT, related_name="attempts")
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="contest_attempts")
|
||||
match = models.ForeignKey(
|
||||
RealtimeMatch, null=True, blank=True, on_delete=models.PROTECT, related_name="attempts"
|
||||
)
|
||||
status = models.CharField(max_length=16, choices=Status.choices, default=Status.ACTIVE)
|
||||
score = models.PositiveIntegerField(default=0)
|
||||
correct_count = models.PositiveIntegerField(default=0)
|
||||
answer_count = models.PositiveIntegerField(default=0)
|
||||
duration_ms = models.PositiveIntegerField(default=0)
|
||||
submission_key = models.CharField(max_length=80, null=True, blank=True)
|
||||
started_at = models.DateTimeField(auto_now_add=True)
|
||||
submitted_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("user", "submission_key"), name="unique_user_contest_submission"
|
||||
)
|
||||
]
|
||||
ordering = ["-started_at"]
|
||||
|
||||
|
||||
class ContestAnswer(models.Model):
|
||||
attempt = models.ForeignKey(ContestAttempt, on_delete=models.CASCADE, related_name="answers")
|
||||
contest_question = models.ForeignKey(ContestQuestion, on_delete=models.PROTECT)
|
||||
submitted_answer = models.CharField(max_length=200)
|
||||
is_correct = models.BooleanField()
|
||||
elapsed_ms = models.PositiveIntegerField()
|
||||
answered_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("attempt", "contest_question"), name="unique_attempt_question_answer"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class RatingHistory(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="rating_history")
|
||||
match = models.ForeignKey(RealtimeMatch, on_delete=models.PROTECT, related_name="rating_changes")
|
||||
rating_before = models.PositiveIntegerField()
|
||||
rating_after = models.PositiveIntegerField()
|
||||
delta = models.SmallIntegerField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class LeaderboardSnapshot(models.Model):
|
||||
contest = models.ForeignKey(Contest, on_delete=models.CASCADE)
|
||||
track = models.CharField(max_length=16, choices=Question.Track.choices)
|
||||
period = models.CharField(max_length=40)
|
||||
rankings = models.JSONField(default=list)
|
||||
generated_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class CheatFlag(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
OPEN = "open", "待处理"
|
||||
DISMISSED = "dismissed", "已忽略"
|
||||
CONFIRMED = "confirmed", "已确认"
|
||||
|
||||
attempt = models.ForeignKey(ContestAttempt, on_delete=models.CASCADE, related_name="cheat_flags")
|
||||
reason = models.CharField(max_length=120)
|
||||
evidence = models.JSONField(default=dict)
|
||||
status = models.CharField(max_length=16, choices=Status.choices, default=Status.OPEN)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
Reference in New Issue
Block a user