Files
Hulumath-Web/backend/contest/models.py
T
Jacky 1f98f7152d
CI / test (push) Failing after 1m31s
v0.2 Preview: Construction Plan.
2026-08-08 21:25:27 +08:00

195 lines
7.3 KiB
Python

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)