from django.contrib.auth.models import AbstractUser from django.db import models class FUser(AbstractUser): phone_number = models.CharField(max_length=15, blank=True, verbose_name='手机号') avatar = models.ImageField(upload_to='avatars/', blank=True, null=True, verbose_name='头像') bio = models.TextField(max_length=500, blank=True, verbose_name='个人简介') isSetPassword = models.BooleanField(default=False, verbose_name='是否设置过密码') gender = models.IntegerField( choices=[(0, '保密'), (1, '男'), (2, '女')], default=0, verbose_name='性别' ) location = models.CharField(max_length=100, blank=True, verbose_name='所在地区') points = models.IntegerField(default=0, verbose_name='积分') coins = models.IntegerField(default=0, verbose_name='y币') cover_image = models.ImageField(upload_to='covers/', blank=True, null=True, verbose_name='个人主页封面') PRIVACY_CHOICES = [ ('public', '公开'), ('friends_only', '仅好友'), ('private', '仅自己'), ] privacy_profile = models.CharField(max_length=20, choices=PRIVACY_CHOICES, default='public', verbose_name='主页可见性') privacy_articles = models.CharField(max_length=20, choices=PRIVACY_CHOICES, default='public', verbose_name='文章可见性') privacy_friends = models.CharField(max_length=20, choices=PRIVACY_CHOICES, default='public', verbose_name='好友列表可见性') notify_email = models.BooleanField(default=True, verbose_name='邮件通知') notify_browser = models.BooleanField(default=True, verbose_name='浏览器通知') notify_reply = models.BooleanField(default=True, verbose_name='回复通知') notify_like = models.BooleanField(default=True, verbose_name='点赞通知') notify_follow = models.BooleanField(default=True, verbose_name='关注通知') notify_system = models.BooleanField(default=True, verbose_name='系统通知') class Meta: verbose_name = '用户' verbose_name_plural = '用户' def save(self, *args, **kwargs): if isinstance(self.gender, str): try: self.gender = int(self.gender) except (ValueError, TypeError): self.gender = 0 if self.gender not in (0, 1, 2): self.gender = 0 super().save(*args, **kwargs) def __str__(self): return self.username class PointTransaction(models.Model): TRANSACTION_TYPES = [ ('earn', '获得'), ('spend', '消费'), ] CURRENCY_TYPES = [ ('points', '积分'), ('coins', 'y币'), ] user = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='transactions', verbose_name='用户') transaction_type = models.CharField(max_length=10, choices=TRANSACTION_TYPES, verbose_name='交易类型') currency_type = models.CharField(max_length=10, choices=CURRENCY_TYPES, verbose_name='货币类型') amount = models.IntegerField(verbose_name='金额') balance_after = models.IntegerField(verbose_name='交易后余额') description = models.CharField(max_length=200, blank=True, verbose_name='描述') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: verbose_name = '积分交易记录' verbose_name_plural = '积分交易记录' ordering = ['-created_at'] def __str__(self): return f"{self.user.username} - {self.get_transaction_type_display()} {self.amount} {self.get_currency_type_display()}" class DailyCheckin(models.Model): user = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='checkins', verbose_name='用户') checkin_date = models.DateField(verbose_name='签到日期') created_at = models.DateTimeField(auto_now_add=True, verbose_name='签到时间') class Meta: verbose_name = '每日签到' verbose_name_plural = '每日签到' unique_together = ('user', 'checkin_date') ordering = ['-checkin_date'] def __str__(self): return f"{self.user.username} - {self.checkin_date}" class Blacklist(models.Model): user = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='blacklist_entries') blocked_user = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='blocked_by') reason = models.CharField(max_length=500, default='', blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '黑名单' verbose_name_plural = '黑名单' unique_together = ('user', 'blocked_user') ordering = ['-created_at'] def __str__(self): return f"{self.user.username} blocked {self.blocked_user.username}" class Follow(models.Model): follower = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='following', verbose_name='关注者') following = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='followers', verbose_name='被关注者') created_at = models.DateTimeField(auto_now_add=True, verbose_name='关注时间') class Meta: verbose_name = '关注关系' verbose_name_plural = '关注关系' unique_together = ('follower', 'following') ordering = ['-created_at'] def __str__(self): return f"{self.follower.username} follows {self.following.username}" class LoginRecord(models.Model): user = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='login_records') device = models.CharField(max_length=200, default='') ip_address = models.GenericIPAddressField(null=True, blank=True) location = models.CharField(max_length=200, default='') login_time = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=20, choices=[('success', '成功'), ('failed', '失败'), ('suspicious', '异常')], default='success') class Meta: ordering = ['-login_time'] verbose_name = '登录记录' verbose_name_plural = '登录记录' def __str__(self): return f"{self.user.username} - {self.login_time}" class Invitation(models.Model): inviter = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='invitations_sent') invitee = models.ForeignKey(FUser, on_delete=models.SET_NULL, null=True, blank=True, related_name='invitations_received') invite_code = models.CharField(max_length=20, unique=True) created_at = models.DateTimeField(auto_now_add=True) is_used = models.BooleanField(default=False) class Meta: ordering = ['-created_at'] class TaskDefinition(models.Model): TASK_TYPE_CHOICES = [ ('daily', '每日任务'), ('weekly', '每周任务'), ('special', '特别任务'), ] ACTION_TYPE_CHOICES = [ ('browse', '浏览页面'), ('learn', '学习时长'), ('post', '发表内容'), ('share', '分享内容'), ('checkin', '签到'), ('profile', '完善资料'), ('bind', '绑定手机/邮箱'), ('invite', '邀请新用户'), ('custom', '自定义'), ] name = models.CharField(max_length=100, verbose_name='任务名称') task_type = models.CharField(max_length=10, choices=TASK_TYPE_CHOICES, verbose_name='任务类型') action_type = models.CharField(max_length=10, choices=ACTION_TYPE_CHOICES, verbose_name='行为类型') description = models.TextField(blank=True, verbose_name='任务描述') target_count = models.IntegerField(default=1, verbose_name='目标次数/时长(分钟)') reward_points = models.IntegerField(default=0, verbose_name='奖励积分') reward_coins = models.IntegerField(default=0, verbose_name='奖励y币') reward_xp = models.IntegerField(default=0, verbose_name='奖励经验值') is_active = models.BooleanField(default=True, verbose_name='是否启用') sort_order = models.IntegerField(default=0, verbose_name='排序') icon = models.CharField(max_length=50, default='TrophyOutlined', verbose_name='图标名称') redirect_url = models.CharField(max_length=200, blank=True, verbose_name='跳转链接') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: verbose_name = '任务定义' verbose_name_plural = '任务定义' ordering = ['task_type', 'sort_order', 'id'] def __str__(self): return f"[{self.get_task_type_display()}] {self.name}" class UserTaskProgress(models.Model): user = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='task_progress', verbose_name='用户') task = models.ForeignKey(TaskDefinition, on_delete=models.CASCADE, related_name='user_progress', verbose_name='任务') current_count = models.IntegerField(default=0, verbose_name='当前进度') is_completed = models.BooleanField(default=False, verbose_name='是否完成') is_claimed = models.BooleanField(default=False, verbose_name='是否已领取') completed_at = models.DateTimeField(null=True, blank=True, verbose_name='完成时间') claimed_at = models.DateTimeField(null=True, blank=True, verbose_name='领取时间') period_key = models.CharField(max_length=20, verbose_name='周期标识') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') class Meta: verbose_name = '用户任务进度' verbose_name_plural = '用户任务进度' unique_together = ('user', 'task', 'period_key') ordering = ['-updated_at'] def __str__(self): return f"{self.user.username} - {self.task.name} ({self.current_count}/{self.task.target_count})" class LevelThreshold(models.Model): level = models.IntegerField(unique=True, verbose_name='等级') xp_required = models.IntegerField(verbose_name='所需经验值') title = models.CharField(max_length=50, verbose_name='等级称号') class Meta: verbose_name = '等级阈值' verbose_name_plural = '等级阈值' ordering = ['level'] def __str__(self): return f"Lv.{self.level} {self.title} ({self.xp_required} XP)" class UserLevel(models.Model): user = models.OneToOneField(FUser, on_delete=models.CASCADE, related_name='user_level', verbose_name='用户') xp = models.IntegerField(default=0, verbose_name='当前经验值') level = models.IntegerField(default=1, verbose_name='当前等级') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') class Meta: verbose_name = '用户等级' verbose_name_plural = '用户等级' def __str__(self): return f"{self.user.username} - Lv.{self.level} ({self.xp} XP)" @property def current_threshold(self): return LevelThreshold.objects.filter(level__lte=self.level).order_by('-level').first() @property def next_threshold(self): return LevelThreshold.objects.filter(level__gt=self.level).order_by('level').first() class Region(models.Model): """地区三级联动表 - 省/市/区""" REGION_LEVEL_CHOICES = [ (1, '省份'), (2, '城市'), (3, '区县'), ] name = models.CharField(max_length=100, verbose_name='地区名称') code = models.CharField(max_length=20, unique=True, verbose_name='地区编码') level = models.IntegerField(choices=REGION_LEVEL_CHOICES, verbose_name='级别') parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children', verbose_name='上级地区') pinyin = models.CharField(max_length=200, blank=True, verbose_name='拼音') sort_order = models.IntegerField(default=0, verbose_name='排序') class Meta: verbose_name = '地区' verbose_name_plural = '地区' ordering = ['level', 'sort_order', 'code'] indexes = [ models.Index(fields=['parent', 'level']), models.Index(fields=['code']), ] def __str__(self): return self.name class UserDevice(models.Model): DEVICE_TYPE_CHOICES = [ ('desktop', '桌面端'), ('mobile', '移动端'), ('tablet', '平板'), ('other', '其他'), ] user = models.ForeignKey(FUser, on_delete=models.CASCADE, related_name='devices', verbose_name='用户') device_name = models.CharField(max_length=100, verbose_name='设备名称') device_type = models.CharField(max_length=20, choices=DEVICE_TYPE_CHOICES, default='desktop', verbose_name='设备类型') browser = models.CharField(max_length=100, blank=True, verbose_name='浏览器') os = models.CharField(max_length=100, blank=True, verbose_name='操作系统') ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址') location = models.CharField(max_length=200, blank=True, verbose_name='登录地点') user_agent = models.TextField(blank=True, verbose_name='User-Agent') last_active = models.DateTimeField(auto_now=True, verbose_name='最后活跃时间') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') is_current = models.BooleanField(default=False, verbose_name='是否当前设备') session_key = models.CharField(max_length=100, blank=True, verbose_name='会话标识') class Meta: verbose_name = '登录设备' verbose_name_plural = '登录设备' ordering = ['-last_active'] def __str__(self): return f"{self.user.username} - {self.device_name} ({self.get_device_type_display()})"