Files
TermiNexus ec43c4dfe6 feat(auth): User.DefaultRoleUUID 默认登录角色(登录自动激活)
- core.User 新增 DefaultRoleUUID(UUIDField 可空)+ 手写迁移 0004
- _SetCurrentRoleCookie 登录/注册优先激活默认角色(须持有且启用),否则回退首个角色
- GetUserRoles 返回 IsDefault 标记;新增 SetDefaultRole 接口(空 RoleUUID=清除默认)
2026-08-22 01:23:34 +08:00

331 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import uuid
import datetime
from django.db import models
from django.contrib.auth.models import BaseUserManager
from gvsdsdk.model_base import QModel
from gvsdsdk.model_utils import UUIDObj
"""模块:Core 数据模型
定义系统核心表:用户、头像、双因素认证、登录日志、账户激活。
所有 ORM 字段与方法遵循帕斯卡命名法(首字母大写、缩写大写、单单词小写)。"""
def _elt_end_time_default():
"""已废弃:ELT 令牌过期时间默认值,仅为迁移历史兼容保留"""
from gvsds.app_config import getint as _app_getint
days = _app_getint('elt', 'TOKEN_LIFETIME_DAYS', fallback=30)
return datetime.datetime.utcnow() + datetime.timedelta(days=days)
class UUIDField(models.BinaryField):
"""UUID 字段:数据库存储为 BINARY(16),Python 侧自动转换 str/bytes/uuid.UUID"""
description = "UUID stored as BINARY(16)"
def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 16)
kwargs.setdefault('editable', False)
super().__init__(*args, **kwargs)
def db_type(self, connection):
vendor = connection.vendor
if vendor == 'mysql':
return 'BINARY(16)'
elif vendor == 'sqlite':
return 'BLOB'
elif vendor == 'postgresql':
return 'BYTEA'
return 'BINARY(16)'
def from_db_value(self, value, expression, connection):
"""数据库读取:BINARY(16) → UUIDObj"""
if value is None:
return None
if isinstance(value, UUIDObj):
return value
return UUIDObj(value)
def get_prep_value(self, value):
"""写入数据库/查询:UUIDObj/str/bytes/uuid.UUID → BINARY(16)"""
if value is None:
return None
if isinstance(value, UUIDObj):
return value.bytes
if isinstance(value, str):
try:
return uuid.UUID(value).bytes
except ValueError:
return None
if isinstance(value, uuid.UUID):
return value.bytes
if isinstance(value, bytes):
return value
return None
def get_prep_lookup(self, lookup_type, value):
"""确保查询时也正确转换 UUID 字符串为 BINARY(16)"""
if lookup_type in ('exact', 'iexact', 'in'):
if value is None:
return None
if lookup_type == 'in' and isinstance(value, (list, tuple)):
return [self.get_prep_value(v) for v in value]
return self.get_prep_value(value)
return super().get_prep_lookup(lookup_type, value)
def value_to_string(self, obj):
"""序列化:返回 UUID 字符串"""
value = self.value_from_object(obj)
if isinstance(value, UUIDObj):
return value.str or ''
if isinstance(value, str):
return value
if isinstance(value, bytes) and len(value) == 16:
return str(uuid.UUID(bytes=value))
if isinstance(value, uuid.UUID):
return str(value)
return ''
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs['max_length']
return name, path, args, kwargs
class UserManager(BaseUserManager):
"""用户管理器——提供用户创建与超级用户创建。
方法名保留蛇形命名以兼容 Django 内部约定。"""
def create_user(self, username, password=None, email='', **extra_fields):
"""创建普通用户"""
if not username:
raise ValueError('The Username field must be set')
user = self.model(
UserUUID=UUIDObj(uuid.uuid4()),
UserName=username,
UserEmail=email or '',
**extra_fields
)
if password:
user.SetPassword(password)
user.save(using=self._db)
return user
def create_superuser(self, username, password=None, **extra_fields):
"""创建超级用户"""
extra_fields.setdefault('UserAccountLicense', 1)
extra_fields.setdefault('UserPositionStatus', 0)
extra_fields.setdefault('IsStaff', True)
extra_fields.setdefault('IsSuperuser', True)
return self.create_user(username, password, **extra_fields)
class User(QModel):
"""用户表——系统核心身份模型"""
UserUUID = UUIDField(primary_key=True, verbose_name='用户UUID')
UserName = models.CharField(max_length=256, unique=True, verbose_name='用户名')
UserPassword = models.CharField(max_length=60, default='', verbose_name='密码') # 无默认密码(防默认口令后门),须经注册/重置流程设置
UserPositionStatus = models.SmallIntegerField(default=0, verbose_name='职位状态')
UserPersonalSignature = models.CharField(max_length=1024, default='尚未及提笔自叙,浅墨先勾鹤影', verbose_name='个人签名')
UserEmail = models.CharField(max_length=256, default='', verbose_name='邮箱')
UserEmailVerify = models.SmallIntegerField(default=0, verbose_name='邮箱验证')
UserAccountLicense = models.SmallIntegerField(default=1, verbose_name='账户许可')
UserGender = models.SmallIntegerField(default=-1, verbose_name='性别')
UserTele = models.CharField(max_length=16, null=True, blank=True, verbose_name='电话')
UserCreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
UserLastLoginDate = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='最后登录')
IsStaff = models.BooleanField(default=False, verbose_name='是否员工')
IsSuperuser = models.BooleanField(default=False, verbose_name='是否超级用户')
# 默认登录角色:登录/注册后自动激活的角色(空 = 取用户首个角色)
DefaultRoleUUID = UUIDField(null=True, blank=True, verbose_name='默认角色UUID')
objects = UserManager()
class Meta:
db_table = 'User'
app_label = 'core'
verbose_name = '用户'
verbose_name_plural = '用户'
def __str__(self):
return self.UserName
@property
def UUIDStr(self):
"""返回 UUID 字符串形式"""
return UUIDObj(self.UserUUID).str
@property
def IsActive(self):
return self.UserAccountLicense == 1
@property
def IsAnonymous(self):
return False
@property
def IsAuthenticated(self):
return True
@property
def PK(self):
return UUIDObj(self.UserUUID)
@property
def ID(self):
return UUIDObj(self.UserUUID)
def SetPassword(self, raw_password):
"""设置密码——bcrypt 哈希"""
import bcrypt
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(raw_password.encode('utf-8'), salt)
self.UserPassword = hashed.decode('utf-8')
def CheckPassword(self, raw_password):
"""验证密码"""
import bcrypt
try:
return bcrypt.checkpw(
raw_password.encode('utf-8'),
self.UserPassword.encode('utf-8')
)
except Exception:
return False
def GetTele(self):
"""获取电话,空则返回默认文本"""
return self.UserTele or '未填写'
def GetCompanies(self):
"""获取用户所属公司列表(通过 UserRole.CompanyUUID 直接关联)"""
from gvsdsdk.auth.managers import CompanyManager
return CompanyManager.GetCompaniesForUser(self.UserUUID)
def GetRoles(self):
"""获取用户角色列表(角色不绑定公司/部门,租户内独立存在)"""
from gvsdsdk.auth.managers import RoleManager
return RoleManager.GetRolesForUser(self.UserUUID)
def GetDepartments(self):
"""获取用户部门列表(通过 UserRoleDepartment 关联表)"""
from gvsdsdk.auth.managers import DepartmentManager
return DepartmentManager.GetDepartmentsForUser(self.UserUUID)
def HasPermission(self, perm_code):
"""检查用户是否拥有指定权限
通过 UserRole → RolePermission → Permission 链路查询,
汇总所有角色的操作权限后判断(委托 gvsdsdk.RoleManager)。
"""
from gvsdsdk.auth.managers import RoleManager
return RoleManager.HasUserPermission(self.UserUUID, perm_code)
# Django auth 框架兼容(内部使用蛇形命名)
@property
def is_active(self):
return self.IsActive
@property
def is_anonymous(self):
return self.IsAnonymous
@property
def is_authenticated(self):
return self.IsAuthenticated
@property
def pk(self):
return self.PK
@property
def id(self):
return self.ID
def set_password(self, raw_password):
self.SetPassword(raw_password)
def check_password(self, raw_password):
return self.CheckPassword(raw_password)
class UserAvatar(QModel):
"""用户头像表"""
UserUUID = UUIDField(primary_key=True, verbose_name='用户UUID')
UserAvatar = models.BinaryField(editable=True, verbose_name='头像数据')
AvatarUploadTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='上传时间')
AvatarFileType = models.CharField(max_length=16, default='png', verbose_name='文件类型')
class Meta:
db_table = 'UserAvatar'
app_label = 'core'
verbose_name = '用户头像'
verbose_name_plural = '用户头像'
class User2FA(QModel):
"""用户双因素认证表"""
User2FAUUID = UUIDField(primary_key=True, verbose_name='2FA UUID')
UserUUID = UUIDField(db_index=True, verbose_name='用户UUID')
SecretKey = models.CharField(max_length=64, verbose_name='密钥')
IsEnabled = models.SmallIntegerField(default=0, verbose_name='是否启用')
Verified = models.SmallIntegerField(default=0, verbose_name='已验证')
LastUsedAt = models.DateTimeField(null=True, blank=True, verbose_name='最后使用')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'User2FA'
app_label = 'core'
verbose_name = '双因素认证'
verbose_name_plural = '双因素认证'
class UserPending2FA(QModel):
"""双因素认证待验证表"""
PendingUUID = UUIDField(primary_key=True, verbose_name='待确认UUID')
UserUUID = UUIDField(unique=True, verbose_name='用户UUID')
SecretKey = models.CharField(max_length=64, verbose_name='密钥')
ExpireAt = models.DateTimeField(verbose_name='过期时间')
class Meta:
db_table = 'UserPending2FA'
app_label = 'core'
verbose_name = '2FA待确认'
verbose_name_plural = '2FA待确认'
class UserLoginLog(QModel):
"""用户登录日志表"""
LogUUID = UUIDField(primary_key=True, verbose_name='日志UUID')
UserUUID = UUIDField(db_index=True, verbose_name='用户UUID')
LoginIP = models.CharField(max_length=39, verbose_name='登录IP')
LoginPlatform = models.CharField(max_length=32, verbose_name='登录平台')
LoginStatus = models.SmallIntegerField(verbose_name='登录状态')
LoginTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='登录时间')
UserAgent = models.CharField(max_length=512, default='', verbose_name='用户代理')
class Meta:
db_table = 'UserLoginLog'
app_label = 'core'
verbose_name = '登录日志'
verbose_name_plural = '登录日志'
class AccountActivation(QModel):
"""账户激活码表"""
AccountActivationCode = models.BinaryField(primary_key=True, verbose_name='激活码')
UserUUID = UUIDField(verbose_name='用户UUID')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'AccountActivation'
app_label = 'core'
verbose_name = '账户激活'
verbose_name_plural = '账户激活'