311 lines
13 KiB
Python
311 lines
13 KiB
Python
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()})" |