Files
Hulumath-Web/backend/contest/models.py
T

289 lines
10 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 MatchType(models.TextChoices):
RANDOM = "random", "随机匹配"
CHALLENGE = "challenge", "联机码约战"
class Status(models.TextChoices):
WAITING = "waiting", "等待对手"
ACTIVE = "active", "进行中"
COMPLETED = "completed", "已完成"
CANCELLED = "cancelled", "已取消"
class GameKind(models.TextChoices):
QUIZ = "quiz", "口算竞速"
SUDOKU = "sudoku", "数独 Timerun"
TWENTY_FOUR = "twenty_four", "24 点竞速"
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
contest = models.ForeignKey(Contest, on_delete=models.PROTECT)
match_type = models.CharField(
max_length=16,
choices=MatchType.choices,
default=MatchType.RANDOM,
)
challenge_code = models.CharField(max_length=8, unique=True, null=True, blank=True)
game_kind = models.CharField(
max_length=20,
choices=GameKind.choices,
default=GameKind.QUIZ,
)
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)
expires_at = models.DateTimeField(null=True, blank=True)
started_at = models.DateTimeField(null=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True)
class Meta:
indexes = [
models.Index(
fields=(
"contest",
"match_type",
"game_kind",
"status",
"player_one_rating",
"created_at",
),
name="matchmaking_mode_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)
question_order = models.JSONField(default=list, blank=True)
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)
class MathGameAttempt(models.Model):
class Kind(models.TextChoices):
SUDOKU = "sudoku", "数独"
TWENTY_FOUR = "twenty_four", "24 点"
class Difficulty(models.TextChoices):
EASY = "easy", "入门"
STANDARD = "standard", "标准"
HARD = "hard", "进阶"
class Status(models.TextChoices):
ACTIVE = "active", "进行中"
COMPLETED = "completed", "已完成"
FAILED = "failed", "未通过"
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="math_game_attempts",
)
match = models.ForeignKey(
RealtimeMatch,
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="game_attempts",
)
kind = models.CharField(max_length=20, choices=Kind.choices)
difficulty = models.CharField(
max_length=16,
choices=Difficulty.choices,
default=Difficulty.STANDARD,
)
puzzle = models.JSONField(default=dict)
solution = models.JSONField(default=dict)
submission = models.JSONField(default=dict, blank=True)
status = models.CharField(max_length=16, choices=Status.choices, default=Status.ACTIVE)
score = models.PositiveIntegerField(default=0)
duration_ms = models.PositiveIntegerField(default=0)
hints_used = models.PositiveSmallIntegerField(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_math_game_submission",
),
models.UniqueConstraint(
fields=("match", "user"),
name="unique_user_realtime_math_game",
),
]
indexes = [
models.Index(
fields=("kind", "status", "-score", "duration_ms"),
name="math_game_ranking_idx",
)
]
ordering = ["-started_at"]