import uuid from django.contrib.auth.models import AbstractUser from django.db import models from django.utils import timezone class User(AbstractUser): class Track(models.TextChoices): BEGINNER = "beginner", "入门" STANDARD = "standard", "标准" ADVANCED = "advanced", "进阶" OPEN = "open", "Open" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) nickname = models.CharField(max_length=40) avatar_url = models.URLField(blank=True) bio = models.CharField(max_length=200, blank=True) track = models.CharField(max_length=16, choices=Track.choices, default=Track.STANDARD) rating = models.PositiveIntegerField(default=1000) def __str__(self): return self.nickname or self.username class InviteCode(models.Model): code = models.CharField(max_length=32, unique=True) group = models.CharField(max_length=80, blank=True) max_uses = models.PositiveIntegerField(default=1) used_count = models.PositiveIntegerField(default=0) expires_at = models.DateTimeField(null=True, blank=True) is_active = models.BooleanField(default=True) created_by = models.ForeignKey( User, null=True, blank=True, on_delete=models.SET_NULL, related_name="invite_codes" ) created_at = models.DateTimeField(auto_now_add=True) @property def is_available(self): return ( self.is_active and self.used_count < self.max_uses and (self.expires_at is None or self.expires_at > timezone.now()) ) def __str__(self): return self.code class InviteCodeUsage(models.Model): invite_code = models.ForeignKey(InviteCode, on_delete=models.PROTECT, related_name="usages") user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="invite_usage") used_at = models.DateTimeField(auto_now_add=True) ip_address = models.GenericIPAddressField(null=True, blank=True) class UserSession(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="login_sessions") session_key = models.CharField(max_length=40, unique=True) user_agent = models.CharField(max_length=300, blank=True) ip_address = models.GenericIPAddressField(null=True, blank=True) last_seen_at = models.DateTimeField(auto_now=True) revoked_at = models.DateTimeField(null=True, blank=True) class VisitorMigration(models.Model): class Status(models.TextChoices): PENDING = "pending", "待确认" APPLIED = "applied", "已迁移" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="visitor_migration") payload = models.JSONField(default=dict) summary = models.JSONField(default=dict) status = models.CharField(max_length=16, choices=Status.choices, default=Status.PENDING) created_at = models.DateTimeField(auto_now_add=True) applied_at = models.DateTimeField(null=True, blank=True) class AuditLog(models.Model): actor = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) action = models.CharField(max_length=80) target_type = models.CharField(max_length=80) target_id = models.CharField(max_length=80, blank=True) reason = models.CharField(max_length=300, blank=True) metadata = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at"]