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) 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", ) 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", ) ] indexes = [ models.Index( fields=("kind", "status", "-score", "duration_ms"), name="math_game_ranking_idx", ) ] ordering = ["-started_at"]