diff --git a/__init__.py b/__init__.py index c78c9d5..c92f0dc 100644 --- a/__init__.py +++ b/__init__.py @@ -150,11 +150,6 @@ def _load_order(): return _mod -def _load_minecraft(): - from gvsdsdk import minecraft as _mod - return _mod - - def _load_models(): from gvsdsdk import models as _mod return _mod @@ -219,6 +214,9 @@ _LAZY_SYMBOLS = { 'RoleManager': ('auth', 'RoleManager'), 'ShopManager': ('auth', 'ShopManager'), 'PositionManager': ('auth', 'PositionManager'), + 'AuthManager': ('auth', 'AuthManager'), + 'AdminUserManager': ('auth', 'AdminUserManager'), + 'UserService': ('auth', 'UserService'), # ── 分佣引擎 ────────────────────────────────────────── 'CommissionEngine': ('commission', 'CommissionEngine'), @@ -264,10 +262,6 @@ _LAZY_SYMBOLS = { 'Order': ('order', 'Order'), 'OrderItem': ('order', 'OrderItem'), - # ── Minecraft 插件扩展 ──────────────────────────────── - 'EasyAuth': ('minecraft', 'EasyAuth'), - 'EasyAuthManager': ('minecraft', 'EasyAuthManager'), - # ── 数据模型 ────────────────────────────────────────── 'User': ('models', 'User'), 'Role': ('models', 'Role'), @@ -310,7 +304,6 @@ _LOADERS = { 'commission': _load_commission, 'payment': _load_payment, 'order': _load_order, - 'minecraft': _load_minecraft, 'models': _load_models, 'fluent': _load_fluent, 'model_base': _load_model_base, diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc index 76d35aa..517497d 100644 Binary files a/__pycache__/__init__.cpython-312.pyc and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/auth/__init__.py b/auth/__init__.py index 00ca8ea..5111673 100644 --- a/auth/__init__.py +++ b/auth/__init__.py @@ -6,7 +6,7 @@ __all__ = [ 'IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'TenantScopedMixin', 'UserScopedMixin', 'PermissionVerifier', 'IsAdminScopeDisabled', 'IsTenantScopeDisabled', 'IsAdmin', 'IsSuperAdmin', 'IsTenantAdmin', - 'RoleManager', + 'RoleManager', 'ShopManager', 'PositionManager', 'AuthManager', 'AdminUserManager', 'UserService', ] @@ -30,8 +30,14 @@ def __getattr__(name): val = getattr(_m, name) globals()[name] = val return val - if name == 'RoleManager': - from gvsdsdk.auth.managers import RoleManager - globals()['RoleManager'] = RoleManager - return RoleManager + if name in ('RoleManager', 'ShopManager', 'PositionManager', 'AuthManager', 'AdminUserManager'): + from gvsdsdk.auth import managers as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name == 'UserService': + from gvsdsdk.auth import user_service as _m + val = getattr(_m, name) + globals()[name] = val + return val raise AttributeError(f"module 'gvsdsdk.auth' has no attribute {name!r}") diff --git a/auth/__pycache__/__init__.cpython-312.pyc b/auth/__pycache__/__init__.cpython-312.pyc index e8b2160..26fefb6 100644 Binary files a/auth/__pycache__/__init__.cpython-312.pyc and b/auth/__pycache__/__init__.cpython-312.pyc differ diff --git a/auth/__pycache__/managers.cpython-312.pyc b/auth/__pycache__/managers.cpython-312.pyc index 105dff0..8828a17 100644 Binary files a/auth/__pycache__/managers.cpython-312.pyc and b/auth/__pycache__/managers.cpython-312.pyc differ diff --git a/auth/__pycache__/user_service.cpython-312.pyc b/auth/__pycache__/user_service.cpython-312.pyc new file mode 100644 index 0000000..c457ab4 Binary files /dev/null and b/auth/__pycache__/user_service.cpython-312.pyc differ diff --git a/auth/managers.py b/auth/managers.py index 57fdf4e..7141b2e 100644 --- a/auth/managers.py +++ b/auth/managers.py @@ -1,8 +1,9 @@ import uuid from django.utils import timezone as _timezone -from gvsdsdk.models import Role, UserRole, RolePermission, Permission, Position, PositionTemplate, PositionTemplatePermission -from gvsdsdk.model_utils import _GetUserModel +from gvsdsdk.models import Role, UserRole, RolePermission, Permission, Position, PositionTemplate, PositionTemplatePermission, Shop, RoleShop, User2FA, UserAvatar, Password +from gvsdsdk.auth.crypto import HashPassword, VerifyPassword +from gvsdsdk.model_utils import _GetUserModel, UUIDObj # NOTE: 类型注解暂缓——RoleManager/PositionManager 静态方法较多,全量补充注解工作量较大且易引入回归,后续单独迭代处理。 @@ -167,6 +168,69 @@ class RoleManager: qs = qs.filter(CompanyUUID=CompanyUUID) return qs.exists() + @staticmethod + def SaveRolePermissions(RoleUUID, CompanyUUID, PermUUIDs, Commit=True): + """批量保存角色权限——按 RoleUUID+CompanyUUID 维度增量更新 + + 逻辑:不在目标列表中的现有关联删除;目标列表中未关联的新增;已存在的保留。 + + Args: + RoleUUID: 角色 UUID(bytes/str/UUIDObj 均可) + CompanyUUID: 公司 UUID + PermUUIDs: 目标权限 UUID 列表(bytes/str 均可,内部去重并过滤无效项) + Commit: 是否包在事务中提交(默认 True) + + Returns: + (Added, Removed, Kept, Total) 统计元组 + """ + target_perm_uuids = set() + for p in PermUUIDs or []: + try: + target_perm_uuids.add(UUIDObj(p).bytes) + except Exception: + continue + + valid_perm_uuids = set( + Permission.objects.filter(PermUUID__in=target_perm_uuids) + .values_list('PermUUID', flat=True) + ) if target_perm_uuids else set() + + def _apply(): + existing_qs = RolePermission.objects.filter( + RoleUUID=RoleUUID, CompanyUUID=CompanyUUID, + ) + existing_perm_uuids = set(existing_qs.values_list('PermUUID', flat=True)) + + to_delete = existing_perm_uuids - valid_perm_uuids + if to_delete: + existing_qs.filter(PermUUID__in=to_delete).delete() + + to_add = valid_perm_uuids - existing_perm_uuids + new_objs = [ + RolePermission( + RolePermUUID=uuid.uuid4().bytes, + RoleUUID=RoleUUID, + PermUUID=p_uuid, + CompanyUUID=CompanyUUID, + ) + for p_uuid in to_add + ] + if new_objs: + RolePermission.objects.bulk_create(new_objs) + + return ( + len(to_add), + len(to_delete), + len(existing_perm_uuids & valid_perm_uuids), + len(valid_perm_uuids), + ) + + if Commit: + from django.db import transaction + with transaction.atomic(): + return _apply() + return _apply() + class PositionManager: """职务管理器——提供人事职务标签创建、岗位模板管理""" @@ -339,3 +403,368 @@ class PositionManager: PositionTemplateUUID=TemplateUUID, PermUUID=PermUUID, ).exists() + +class ShopManager: + """商铺管理器——商铺实体与角色-商铺关联管理""" + + @staticmethod + def CreateShop(Name, CompanyUUID, Code='', ShopType=0, + Address='', Contact='', Phone='', Desc=''): + """创建商铺 + + Args: + Name: 商铺名称 + CompanyUUID: 所属公司 UUID(UUIDObj/str/bytes 均可) + Code: 商铺编码 + ShopType: 商铺类型(0=默认) + Address: 商铺地址 + Contact: 联系人 + Phone: 联系电话 + Desc: 描述 + + Returns: + Shop 实例 + """ + shop = Shop( + ShopUUID=uuid.uuid4().bytes, + CompanyUUID=CompanyUUID, + ShopName=Name, + ShopCode=Code, + ShopType=ShopType, + ShopStatus=1, + ShopAddress=Address, + ShopContact=Contact, + ShopPhone=Phone, + ShopDesc=Desc, + CreateTime=_timezone.now(), + UpdateTime=_timezone.now(), + ) + shop.save() + return shop + + @staticmethod + def GetShopByUUID(ShopUUID): + """按 UUID 查询商铺(不存在返回 None)""" + return Shop.objects.filter(ShopUUID=ShopUUID).first() + + @staticmethod + def SearchShops(Keyword='', CompanyUUID=None, Limit=20): + """搜索商铺(按名称模糊匹配,可选公司过滤,仅启用) + + Args: + Keyword: 商铺名称关键字 + CompanyUUID: 可选,限定公司范围 + Limit: 返回条数上限 + + Returns: + QuerySet[Shop] + """ + qs = Shop.objects.filter(ShopStatus=1) + if Keyword: + qs = qs.filter(ShopName__icontains=Keyword) + if CompanyUUID is not None: + qs = qs.filter(CompanyUUID=CompanyUUID) + return qs[:Limit] + + @staticmethod + def GetShopsForRole(RoleUUID): + """获取角色关联的所有商铺""" + shop_uuids = RoleShop.objects.filter( + RoleUUID=RoleUUID, + ).values_list('ShopUUID', flat=True) + return Shop.objects.filter(ShopUUID__in=shop_uuids) + + @staticmethod + def GetMainShopForRole(RoleUUID): + """获取角色的主商铺(IsMainShop=1;不存在返回 None)""" + rs = RoleShop.objects.filter(RoleUUID=RoleUUID, IsMainShop=1).first() + if not rs: + return None + return Shop.objects.filter(ShopUUID=rs.ShopUUID).first() + + @staticmethod + def AssignRole(RoleUUID, ShopUUID, IsMainShop=0): + """为角色关联商铺 + + Args: + RoleUUID: 角色 UUID + ShopUUID: 商铺 UUID + IsMainShop: 是否设为主商铺(1=是) + + Returns: + RoleShop 实例 + """ + rs = RoleShop( + RoleShopUUID=uuid.uuid4().bytes, + RoleUUID=RoleUUID, + ShopUUID=ShopUUID, + IsMainShop=IsMainShop, + CreateTime=_timezone.now(), + ) + rs.save() + return rs + + @staticmethod + def RemoveRole(RoleUUID, ShopUUID): + """移除角色与商铺的关联;返回是否命中""" + deleted, _ = RoleShop.objects.filter( + RoleUUID=RoleUUID, ShopUUID=ShopUUID, + ).delete() + return deleted > 0 + + +class AuthManager: + """用户认证管理器——账号查询、创建、密码、2FA 与头像""" + + @staticmethod + def GetUserByUserName(UserName): + """按用户名查询用户(不存在返回 None)""" + if not UserName: + return None + User = _GetUserModel() + return User.objects.filter(UserName=UserName).first() + + @staticmethod + def GetUserByUUID(UserUUID): + """按 UUID 查询用户(不存在返回 None)""" + if not UserUUID: + return None + User = _GetUserModel() + return User.objects.filter(UserUUID=UUIDObj(UserUUID).bytes).first() + + @staticmethod + def IsUserNameTaken(UserName): + """用户名是否已被占用""" + if not UserName: + return False + User = _GetUserModel() + return User.objects.filter(UserName=UserName).exists() + + @staticmethod + def CreateUser(UserName, Email, Password): + """创建用户(默认启用账户、在职)并设置 bcrypt 密码 + + Args: + UserName: 用户名 + Email: 邮箱 + Password: 明文密码(内部做 bcrypt 哈希) + + Returns: + User 实例 + """ + User = _GetUserModel() + user = User( + UserUUID=UUIDObj(uuid.uuid4()), + UserName=UserName, + UserEmail=Email, + UserAccountLicense=1, + UserPositionStatus=1, + ) + user.SetPassword(Password) + user.save() + return user + + @staticmethod + def Check2FARequired(UserUUID): + """检查用户是否启用 2FA + + Args: + UserUUID: 用户 UUID(bytes/str/UUIDObj 均可) + + Returns: + (是否启用, User2FA 实例或 None) + """ + two_fa = User2FA.objects.filter(UserUUID=UUIDObj(UserUUID).bytes).first() + if two_fa is None: + return False, None + return two_fa.IsEnabled == 1, two_fa + + @staticmethod + def Verify2FA(UserUUID, Code): + """校验 TOTP 2FA 验证码;返回是否匹配""" + enabled, two_fa = AuthManager.Check2FARequired(UserUUID) + if not enabled or two_fa is None or not two_fa.SecretKey or not Code: + return False + import pyotp + return pyotp.TOTP(two_fa.SecretKey).verify(Code, valid_window=1) + + @staticmethod + def UpdateLastLogin(UserUUID): + """更新用户最后登录时间""" + user = AuthManager.GetUserByUUID(UserUUID) + if user is None: + return + user.UserLastLoginDate = _timezone.now() + user.save(update_fields=['UserLastLoginDate']) + + @staticmethod + def ChangePassword(UserUUID, OldPassword, NewPassword): + """修改密码 + + Args: + UserUUID: 用户 UUID + OldPassword: 原密码(明文) + NewPassword: 新密码(明文) + + Returns: + (成功?, 消息) + """ + user = AuthManager.GetUserByUUID(UserUUID) + if user is None: + return False, '用户不存在' + if not user.CheckPassword(OldPassword): + return False, '原密码错误' + user.SetPassword(NewPassword) + user.save(update_fields=['UserPassword']) + return True, '密码修改成功' + + @staticmethod + def GetAvatar(UserUUID): + """获取用户头像 + + Args: + UserUUID: 用户 UUID(bytes/str/UUIDObj 均可) + + Returns: + (头像 bytes, 文件类型);未设置返回 None + """ + avatar = UserAvatar.objects.filter(UserUUID=UUIDObj(UserUUID).bytes).first() + if avatar is None: + return None + return avatar.UserAvatar, avatar.AvatarFileType + + @staticmethod + def SetAvatar(UserUUID, AvatarBytes, FileType): + """设置用户头像(更新或创建) + + Args: + UserUUID: 用户 UUID + AvatarBytes: 头像图片二进制 + FileType: 文件类型(png/jpeg) + + Returns: + UserAvatar 实例 + """ + avatar, _ = UserAvatar.objects.get_or_create( + UserUUID=UUIDObj(UserUUID).bytes, + ) + avatar.UserAvatar = AvatarBytes + avatar.AvatarFileType = FileType + avatar.save(update_fields=['UserAvatar', 'AvatarFileType', 'AvatarUploadTime']) + return avatar + + +class AdminUserManager: + """管理端用户管理器——用户搜索、密码重置、账户状态与邮箱管理""" + + @staticmethod + def SearchUsers(Keyword='', Offset=0, PageSize=20): + """搜索用户(按用户名模糊匹配,分页) + + Args: + Keyword: 用户名关键字 + Offset: 偏移量 + PageSize: 每页条数 + + Returns: + (list[dict], total) + """ + User = _GetUserModel() + qs = User.objects.all() + if Keyword: + qs = qs.filter(UserName__icontains=Keyword) + total = qs.count() + users = qs[Offset:Offset + PageSize] + records = [] + for u in users: + records.append({ + 'UserUUID': UUIDObj(u.UserUUID).str, + 'UserName': u.UserName, + 'UserEmail': u.UserEmail or '', + 'UserEmailVerify': u.UserEmailVerify, + 'UserGender': u.UserGender, + 'UserAccountLicense': u.UserAccountLicense, + 'UserPositionStatus': u.UserPositionStatus, + }) + return records, total + + @staticmethod + def GetUserByUUID(UserUUID): + """按 UUID 查询用户(不存在返回 None)""" + return AuthManager.GetUserByUUID(UserUUID) + + @staticmethod + def ResetPassword(UserUUID, NewPassword): + """重置用户密码(写入独立 Password 表,bcrypt 哈希) + + Returns: + (成功?, 消息) + """ + user = AuthManager.GetUserByUUID(UserUUID) + if user is None: + return False, '用户不存在' + pw_obj, _ = Password.objects.get_or_create(UserUUID=user.UserUUID) + pw_obj.Password = HashPassword(NewPassword) + pw_obj.save(update_fields=['Password']) + return True, '密码重置成功' + + @staticmethod + def VerifyPassword(UserUUID, Password): + """校验用户密码(独立 Password 表) + + Returns: + (用户记录存在?, 密码是否匹配) + """ + pw_obj = Password.objects.filter(UserUUID=UUIDObj(UserUUID).bytes).first() + if pw_obj is None: + return False, False + try: + return True, VerifyPassword(Password, pw_obj.Password) + except (ValueError, TypeError): + return True, False + + @staticmethod + def SetAccountLicense(UserUUID, License): + """设置账户启用状态(License=1 启用,0 禁用) + + Returns: + (成功?, 消息) + """ + user = AuthManager.GetUserByUUID(UserUUID) + if user is None: + return False, '用户不存在' + user.UserAccountLicense = License + user.save(update_fields=['UserAccountLicense']) + return True, '账户已启用' if License == 1 else '账户已禁用' + + @staticmethod + def SetUserEmail(UserUUID, Email): + """设置用户邮箱 + + Returns: + (成功?, 消息) + """ + user = AuthManager.GetUserByUUID(UserUUID) + if user is None: + return False, '用户不存在' + user.UserEmail = Email + user.save(update_fields=['UserEmail']) + return True, '邮箱验证已发送' + + @staticmethod + def Get2FAInfo(UserUUID): + """查询用户 2FA 信息(不含 SecretKey,避免密钥泄露) + + Returns: + dict;未配置返回 None + """ + two_fa = User2FA.objects.filter(UserUUID=UUIDObj(UserUUID).bytes).first() + if two_fa is None: + return None + return { + 'IsEnabled': two_fa.IsEnabled, + 'Verified': two_fa.Verified, + 'LastUsedAt': two_fa.LastUsedAt.isoformat() if two_fa.LastUsedAt else None, + 'CreateTime': two_fa.CreateTime.isoformat() if two_fa.CreateTime else None, + } + diff --git a/auth/user_service.py b/auth/user_service.py new file mode 100644 index 0000000..c37a150 --- /dev/null +++ b/auth/user_service.py @@ -0,0 +1,107 @@ +"""gvsdsdk.auth.user_service — 用户信息服务 + +聚合用户基本信息、角色、权限与订单,供管理端用户详情展示。 +角色权限语义与 ``RoleManager`` 保持一致;公司名称等 SAAS 专属数据 +由调用方(gvsds 视图层)自行补充。 +""" + +from gvsdsdk.model_utils import UUIDObj + +from gvsdsdk.auth.managers import AuthManager, RoleManager + + +class UserService: + """用户信息服务——聚合用户、角色、权限、订单信息""" + + @staticmethod + def GetUserDetail(UserUUID): + """聚合查询用户详情 + + Args: + UserUUID: 用户 UUID(bytes/str/UUIDObj 均可) + + Returns: + dict,含 ``User``/``Roles``/``Permissions``/``Orders`` 四段; + 用户不存在返回 ``None`` + """ + user = AuthManager.GetUserByUUID(UserUUID) + if user is None: + return None + + user_info = { + 'UserUUID': UUIDObj(user.UserUUID).str, + 'UserName': user.UserName, + 'UserEmail': user.UserEmail or '', + 'UserEmailVerify': user.UserEmailVerify, + 'UserTele': user.UserTele or '', + 'UserGender': user.UserGender, + 'UserAccountLicense': user.UserAccountLicense, + 'UserPositionStatus': user.UserPositionStatus, + 'UserCreateTime': str(user.UserCreateTime) if user.UserCreateTime else '', + 'UserLastLoginDate': str(user.UserLastLoginDate) if user.UserLastLoginDate else '', + 'IsStaff': user.IsStaff, + 'IsSuperuser': user.IsSuperuser, + } + + # 角色列表(保留行级信息:CompanyUUID、RoleName 覆盖) + roles = [] + user_roles = RoleManager.GetUserRoleDetails(user.UserUUID) + for ur in user_roles: + role = RoleManager.GetRoleByUUID(ur.RoleUUID) + if role is None: + continue + roles.append({ + 'RoleUUID': UUIDObj(role.RoleUUID).str if role.RoleUUID else '', + 'RoleName': ur.RoleName or role.RoleName, + 'RoleType': role.RoleType, + 'RoleStatus': role.RoleStatus, + 'AssignScope': role.AssignScope, + 'RoleDesc': role.RoleDesc or '', + 'CompanyUUID': UUIDObj(ur.CompanyUUID).str if ur.CompanyUUID else '', + }) + + # 权限列表(按 PermCode 去重,仅启用权限) + permissions = [] + seen = set() + for ur in user_roles: + if not ur.RoleUUID: + continue + for perm in RoleManager.GetPermissionsForRole(ur.RoleUUID): + key = perm.PermCode + if key in seen: + continue + seen.add(key) + permissions.append({ + 'PermName': perm.PermName, + 'PermCode': perm.PermCode, + 'PermCategory': perm.PermCategory or '', + 'PermDesc': perm.PermDesc or '', + }) + + # 订单列表(最近 20 条,表不存在时静默跳过) + orders = [] + try: + from gvsdsdk.order.models import Order + user_orders = Order.objects.filter( + BuyerUserUUID=user.UserUUID, + ).order_by('-CreateTime')[:20] + for o in user_orders: + orders.append({ + 'OrderNO': o.OrderNO, + 'OrderTitle': o.OrderTitle, + 'OrderType': o.OrderType, + 'TotalAmount': str(o.TotalAmount), + 'PayAmount': str(o.PayAmount), + 'Currency': o.Currency, + 'OrderStatus': o.OrderStatus, + 'CreateTime': str(o.CreateTime) if o.CreateTime else '', + }) + except Exception: + pass # Order 表可能不存在 + + return { + 'User': user_info, + 'Roles': roles, + 'Permissions': permissions, + 'Orders': orders, + } diff --git a/minecraft/__init__.py b/minecraft/__init__.py deleted file mode 100644 index d54ebd4..0000000 --- a/minecraft/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -"""gvsdsdk.minecraft — Minecraft 服务器插件扩展模块 - -提供 MC 插件共享表的 ORM 映射与认证服务: - -- ``EasyAuth``:EasyAuth 插件认证表(managed=False,外部 schema) -- ``EasyAuthManager``:游戏账号密码验证、查询、绑定标记管理 - -注:本模块的模型 ``app_label='mc'``,在主服务器(gvsds)侧通过 -``gvsds/db_router.py`` 路由到 minecraft 库;子服务器一般不需要访问。 -""" - -from .models import EasyAuth -from .auth import EasyAuthManager - -__all__ = [ - 'EasyAuth', - 'EasyAuthManager', -] diff --git a/minecraft/__pycache__/__init__.cpython-312.pyc b/minecraft/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index ac00e42..0000000 Binary files a/minecraft/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/minecraft/__pycache__/auth.cpython-312.pyc b/minecraft/__pycache__/auth.cpython-312.pyc deleted file mode 100644 index 9efa3fb..0000000 Binary files a/minecraft/__pycache__/auth.cpython-312.pyc and /dev/null differ diff --git a/minecraft/__pycache__/models.cpython-312.pyc b/minecraft/__pycache__/models.cpython-312.pyc deleted file mode 100644 index 1def803..0000000 Binary files a/minecraft/__pycache__/models.cpython-312.pyc and /dev/null differ diff --git a/minecraft/auth.py b/minecraft/auth.py deleted file mode 100644 index 90d716f..0000000 --- a/minecraft/auth.py +++ /dev/null @@ -1,146 +0,0 @@ -"""模块:gvsdsdk.minecraft.auth — EasyAuth 插件认证与绑定管理 - -提供: - - ``VerifyPassword``:使用 bcrypt 校验游戏账号密码 - - ``EasyAuthManager``:游戏账号查询、密码验证、绑定标记读写 -""" - -import bcrypt - - -def VerifyPassword(plain_password: str, bcrypt_hash: str) -> bool: - """校验明文密码与 bcrypt hash 是否匹配 - - Args: - plain_password: 玩家输入的明文密码 - bcrypt_hash: ``easyauth.data.password`` 中存储的 bcrypt hash 字符串 - - Returns: - True 如果密码匹配;False 如果不匹配或 hash 格式异常 - """ - if not plain_password or not bcrypt_hash: - return False - try: - return bcrypt.checkpw(plain_password.encode('utf-8'), bcrypt_hash.encode('utf-8')) - except (ValueError, TypeError): - return False - - -class EasyAuthManager: - """EasyAuth 认证管理器——游戏账号查询、密码验证、绑定标记读写""" - - @staticmethod - def GetByUsername(username: str): - """按游戏名(不区分大小写)查询 EasyAuth 记录 - - Args: - username: 游戏名(原始大小写均可) - - Returns: - ``EasyAuth`` 实例;不存在则返回 ``None`` - """ - from .models import EasyAuth - if not username: - return None - return EasyAuth.objects.filter(UsernameLower=username.lower()).first() - - @staticmethod - def VerifyPassword(easyauth, plain_password: str) -> bool: - """校验 EasyAuth 记录的密码 - - Args: - easyauth: ``EasyAuth`` 实例 - plain_password: 明文密码 - - Returns: - True 如果密码匹配;False 如果不匹配、未设置密码或 hash 异常 - """ - if easyauth is None: - return False - data = easyauth.Data or {} - password_hash = data.get('password') or '' - return VerifyPassword(plain_password, password_hash) - - @staticmethod - def IsBound(easyauth) -> bool: - """判断该游戏账号是否已绑定 GVSDS 用户""" - return easyauth is not None and easyauth.UserRoleUUID is not None - - @staticmethod - def MarkBound(easyauth, user_role_uuid) -> int: - """将 UserRoleUUID 写入 easyauth 作为绑定标记 - - 使用乐观锁(``UserRoleUUID__isnull=True``)防止并发重复绑定。 - - Args: - easyauth: ``EasyAuth`` 实例 - user_role_uuid: 新创建的 UserRole 关联 UUID(str / bytes / UUIDObj 均可) - - Returns: - 实际更新行数:1 成功,0 表示已被其他请求抢先绑定 - """ - from .models import EasyAuth - if easyauth is None: - return 0 - return EasyAuth.objects.filter( - AuthID=easyauth.AuthID, - UserRoleUUID__isnull=True, - ).update(UserRoleUUID=user_role_uuid) - - @staticmethod - def ClearBound(easyauth) -> int: - """清空 easyauth 的 UserRoleUUID(用于解绑场景)""" - from .models import EasyAuth - if easyauth is None: - return 0 - return EasyAuth.objects.filter( - AuthID=easyauth.AuthID, - ).update(UserRoleUUID=None) - - @staticmethod - def GetPlayerUUIDByGVSDSUser(gvsds_user_uuid): - """GVSDS UserUUID → MC 玩家 UUID 字符串(varchar36) - - 反查路径:GVSDS UserUUID → 该用户的 UserRoleUUID 集合 → - easyauth 表中 UserRoleUUID 在该集合内的记录 → PlayerUUID。 - - 与 MC 绑定流程(``UserBindView``)配套:绑定时会把新建的 - UserRoleUUID 写入 easyauth.UserRoleUUID 列作为绑定标记, - 因此反过来即可定位该 GVSDS 用户绑定的 MC 玩家。 - - Args: - gvsds_user_uuid: GVDS 用户 UUID(bytes/str/UUIDObj 均可) - - Returns: - MC 玩家 UUID 字符串;未绑定或 easyauth 记录不存在时返回 None - """ - from .models import EasyAuth - from gvsdsdk.models import UserRole - from gvsdsdk.model_utils import UUIDObj - if not gvsds_user_uuid: - return None - raw_bytes = UUIDObj(gvsds_user_uuid).bytes - # 该 GVSDS 用户名下的所有 UserRoleUUID(跨公司/角色) - user_role_uuids = list( - UserRole.objects.filter(UserUUID=raw_bytes).values_list('UserRoleUUID', flat=True) - ) - if not user_role_uuids: - return None - # easyauth 表反查绑定标记,定位 MC 玩家 - ea = EasyAuth.objects.filter(UserRoleUUID__in=user_role_uuids).first() - return ea.PlayerUUID if ea else None - - @staticmethod - def GetByPlayerUUID(player_uuid): - """按 MC 玩家 UUID 字符串查询 EasyAuth 记录 - - Args: - player_uuid: MC 玩家 UUID 字符串(easyauth.uuid 列,varchar36) - - Returns: - ``EasyAuth`` 实例;不存在或入参为空则返回 ``None`` - """ - from .models import EasyAuth - if not player_uuid: - return None - return EasyAuth.objects.filter(PlayerUUID=player_uuid).first() diff --git a/minecraft/models.py b/minecraft/models.py deleted file mode 100644 index 86a3f2d..0000000 --- a/minecraft/models.py +++ /dev/null @@ -1,44 +0,0 @@ -"""模块:gvsdsdk.minecraft.models — MC 插件共享表 ORM 映射 - -当前仅包含 ``EasyAuth`` 表(EasyAuth 插件认证表)。 - -设计要点: - - ``app_label='mc'``:模型归属 gvsds.apps.mc app,由 gvsds 主服务器的 - ``gvsds/db_router.py`` 路由到 minecraft 库 - - ``managed=False``:表由 MC 插件管理,Django 不参与迁移 - - ``UserRoleUUID`` 列由本系统 ALTER 添加(BINARY(16) 可空), - 存绑定后创建的 ``UserRole`` 关联 UUID -""" - -from gvsdsdk.model_base import QModel -from gvsdsdk.models import UUIDField -from django.db import models - - -class EasyAuth(QModel): - """EasyAuth 认证表——MC EasyAuth 插件的玩家凭据 - - 映射真实表 ``easyauth``(managed=False)。``data`` 列为 JSON, - 其中 ``password`` 字段为 bcrypt hash(``$2a$12$...``), - 验证时使用 ``bcrypt.checkpw``。 - - ``UserRoleUUID`` 列由本系统 ALTER 添加(BINARY(16) 可空), - 存绑定后创建的 ``UserRole`` 关联 UUID,用于 MC 插件侧反查 GVSDS 身份。 - """ - - AuthID = models.AutoField(primary_key=True, db_column='id', verbose_name='认证ID') - Username = models.CharField(max_length=255, unique=True, db_column='username', verbose_name='玩家名') - UsernameLower = models.CharField(max_length=255, db_column='username_lower', verbose_name='玩家名小写') - PlayerUUID = models.CharField(max_length=255, null=True, blank=True, db_column='uuid', verbose_name='MC玩家UUID字符串') - Data = models.JSONField(db_column='data', verbose_name='插件数据') - UserRoleUUID = UUIDField(null=True, blank=True, db_column='UserRoleUUID', verbose_name='绑定用户角色关联UUID') - - class Meta: - db_table = 'easyauth' - app_label = 'mc' - managed = False - verbose_name = 'EasyAuth认证' - verbose_name_plural = 'EasyAuth认证' - - def __str__(self): - return self.Username