diff --git a/gvsds/apps/crb/business.py b/gvsds/apps/crb/business.py index 52fc28d0..4ba1d44e 100644 --- a/gvsds/apps/crb/business.py +++ b/gvsds/apps/crb/business.py @@ -4,7 +4,7 @@ Tenant/Company)。业务规则移植自旧 Flask 实现: - ``StoreValueRule``:6 种储值方案规则表(活期/定期×5 档) -- ``GetGVSDSUserToMCPlayerUUID``:委托 ``gvsdsdk.minecraft.EasyAuthManager`` +- ``GetGVSDSUserToMCPlayerUUID``:委托 ``gvsds.apps.mc.manager.EasyAuthManager`` 反查 easyauth 表的 UserRoleUUID 绑定标记,把 GVSDS UserUUID 映射为 MC 玩家 UUID 字符串(与 MC 绑定流程保持一致) - ``GetUserByUUID`` / ``GetUserByName``:通过业务子类的 User 模型按 @@ -45,14 +45,14 @@ StoreValueRule: dict = { def GetGVSDSUserToMCPlayerUUID(gvsds_user_uuid) -> Optional[str]: """GVSDS UserUUID → MC 玩家 UUID 字符串(varchar36) - 委托 ``gvsdsdk.minecraft.EasyAuthManager.GetPlayerUUIDByGVSDSUser`` 反查 + 委托 ``gvsds.apps.mc.manager.EasyAuthManager.GetPlayerUUIDByGVSDSUser`` 反查 easyauth 表的 UserRoleUUID 绑定标记,与 MC 绑定流程(``UserBindView``) 保持一致。旧 Flask 时代使用的 ``CmeWalletGVSDSMapping`` 表已被新绑定 机制取代,不再写入数据,因此不再作为查询源。 接受 bytes/str/UUIDObj 形式的 GVSDS UserUUID。 """ - from gvsdsdk.minecraft import EasyAuthManager + from gvsds.apps.mc.manager import EasyAuthManager return EasyAuthManager.GetPlayerUUIDByGVSDSUser(gvsds_user_uuid) @@ -71,7 +71,7 @@ def GetPlayerNameByGVSDSUser(gvsds_user_uuid) -> Optional[str]: player_uuid = GetGVSDSUserToMCPlayerUUID(gvsds_user_uuid) if not player_uuid: return None - from gvsdsdk.minecraft import EasyAuthManager + from gvsds.apps.mc.manager import EasyAuthManager ea = EasyAuthManager.GetByPlayerUUID(player_uuid) return ea.Username if ea else None @@ -90,7 +90,7 @@ def GetGVSDSUserByPlayerName(player_name: str): """ if not player_name: return None - from gvsdsdk.minecraft.models import EasyAuth + from gvsds.apps.mc.models import EasyAuth from gvsdsdk.models import UserRole from gvsdsdk.model_utils import UUIDObj ea = EasyAuth.objects.filter(Username=player_name).first() diff --git a/gvsds/apps/crb/views.py b/gvsds/apps/crb/views.py index 88d432f9..2ee42eab 100644 --- a/gvsds/apps/crb/views.py +++ b/gvsds/apps/crb/views.py @@ -1403,7 +1403,7 @@ class CmeStoreValueViewSet(CompanyPrivateAccessMixin, viewsets.ModelViewSet): # 玩家名从 easyauth 表取(已绑定后的 MC 玩家名) player_name = '' try: - from gvsdsdk.minecraft import EasyAuthManager + from gvsds.apps.mc.manager import EasyAuthManager if player_uuid_str: ea = EasyAuthManager.GetByPlayerUUID(player_uuid_str) if ea: diff --git a/gvsds/apps/gerp/views/role_permission.py b/gvsds/apps/gerp/views/role_permission.py index 69e8cff0..180ff414 100644 --- a/gvsds/apps/gerp/views/role_permission.py +++ b/gvsds/apps/gerp/views/role_permission.py @@ -7,11 +7,10 @@ import logging import uuid as _uuid -from django.db import transaction from rest_framework.response import Response from . import ELTAuthenticatedAPIView -from gvsds.apps.gerp.models import Role, Permission, RolePermission, Company +from gvsds.apps.gerp.models import Permission, RolePermission, Company from gvsds.apps.gerp.views.form_api import _is_admin, _is_super_admin, _get_current_context logger = logging.getLogger(__name__) @@ -180,54 +179,19 @@ class RoleSavePermissionsView(ELTAuthenticatedAPIView): return Response({'code': 1, 'msg': 'CompanyUUID 格式无效'}, status=400) # 校验角色和公司存在 - if not Role.objects.filter(RoleUUID=role_uuid).exists(): + from gvsdsdk.auth.managers import RoleManager + if RoleManager.GetRoleByUUID(role_uuid) is None: return Response({'code': 1, 'msg': '角色不存在'}, status=404) if not Company.objects.filter(CompanyUUID=company_uuid).exists(): return Response({'code': 1, 'msg': '公司不存在'}, status=404) - # 规范化 PermUUIDs(去重 + 转 bytes + 过滤无效) - target_perm_uuids = set() - for p_str in perm_uuid_list: - p_bytes = _to_uuid_bytes(p_str) - if p_bytes: - target_perm_uuids.add(p_bytes) - - # 校验权限是否真实存在(避免脏数据) - valid_perm_uuids = set( - Permission.objects.filter(PermUUID__in=target_perm_uuids).values_list('PermUUID', flat=True) - ) if target_perm_uuids else set() - + # 规范化 PermUUIDs(去重 + 转 bytes + 过滤无效)并校验存在性、增量更新 try: - with transaction.atomic(): - # 现有权限集合 - existing_qs = RolePermission.objects.filter( - RoleUUID=role_uuid, - CompanyUUID=company_uuid, - ) - 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=role_uuid, - PermUUID=p_uuid, - CompanyUUID=company_uuid, - ) - for p_uuid in to_add - ] - if new_objs: - RolePermission.objects.bulk_create(new_objs) - - added_count = len(to_add) - removed_count = len(to_delete) - kept_count = len(existing_perm_uuids & valid_perm_uuids) + added_count, removed_count, kept_count, total = RoleManager.SaveRolePermissions( + RoleUUID=role_uuid, + CompanyUUID=company_uuid, + PermUUIDs=perm_uuid_list, + ) except Exception as e: logger.exception('[RoleSavePermissions] 保存角色权限失败') return Response({'code': 1, 'msg': f'保存失败: {e}'}, status=500) @@ -239,6 +203,6 @@ class RoleSavePermissionsView(ELTAuthenticatedAPIView): 'Added': added_count, 'Removed': removed_count, 'Kept': kept_count, - 'Total': len(valid_perm_uuids), + 'Total': total, } }) diff --git a/gvsds/apps/gerp/views/site_user.py b/gvsds/apps/gerp/views/site_user.py index adbe55d7..01dc9992 100644 --- a/gvsds/apps/gerp/views/site_user.py +++ b/gvsds/apps/gerp/views/site_user.py @@ -2,11 +2,9 @@ 提供管理员对用户的管理功能:列表、密码重置、启用/禁用、邮箱验证、背包管理。""" import uuid -import bcrypt from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from gvsds.apps.core.models import User, User2FA -from gvsds.apps.gerp.models import Password, Backpack, ItemReference +from gvsds.apps.gerp.models import Backpack, ItemReference from gvsdsdk.model_utils import UUIDObj from gvsdsdk.auth.permissions import IsAdminUser from . import ELTAuthenticatedAPIView @@ -22,25 +20,13 @@ class SiteUserManagementView(ELTAuthenticatedAPIView): permission_classes = [IsAuthenticated] def get(self, request): + from gvsdsdk.auth.managers import AdminUserManager keyword = request.query_params.get('keyword', '') - qs = User.objects.all() - if keyword: - qs = qs.filter(UserName__icontains=keyword) offset = int(request.query_params.get('offset', 0)) page_size = int(request.query_params.get('PageSize', 20)) - users = qs[offset:offset + page_size] - total = qs.count() - data = [] - for u in users: - data.append({ - 'UserUUID': UUIDObj(u.UserUUID).str, - 'UserName': u.UserName, - 'UserEmail': u.UserEmail, - 'UserEmailVerify': u.UserEmailVerify, - 'UserGender': u.UserGender, - 'UserAccountLicense': u.UserAccountLicense, - 'UserPositionStatus': u.UserPositionStatus, - }) + data, total = AdminUserManager.SearchUsers( + Keyword=keyword, Offset=offset, PageSize=page_size, + ) # data 用对象包裹(含分页元数据): # 前端 axios 拦截器解包 {code, data, ...} 为 result.data, # data 对象含 list/has_more/total,EntityPicker 从 list 取选项、从 has_more 判断是否还有更多 @@ -60,16 +46,14 @@ class SiteUserResetPasswordView(ELTAuthenticatedAPIView): def post(self, request): ser = SiteUserResetPasswordSerializer(data=request.data) ser.IsValid(raise_exception=True) - user_uuid_str = ser.ValidatedData['UserUUID'] - try: - user_bytes = UUIDObj(user_uuid_str).bytes - pw_obj = Password.objects.get(UserUUID=user_bytes) - new_hash = bcrypt.hashpw(ser.ValidatedData['NewPassword'].encode(), bcrypt.gensalt()).decode() - pw_obj.Password = new_hash - pw_obj.save(update_fields=['Password']) - return Response({'code': 0, 'msg': '密码重置成功'}) - except (Password.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '用户不存在'}, status=404) + from gvsdsdk.auth.managers import AdminUserManager + ok, msg = AdminUserManager.ResetPassword( + ser.ValidatedData['UserUUID'], + ser.ValidatedData['NewPassword'], + ) + if not ok: + return Response({'code': 1, 'msg': msg}, status=404) + return Response({'code': 0, 'msg': '密码重置成功'}) class SiteUserVerifyPasswordView(ELTAuthenticatedAPIView): @@ -78,13 +62,14 @@ class SiteUserVerifyPasswordView(ELTAuthenticatedAPIView): def post(self, request): ser = SiteUserVerifyPasswordSerializer(data=request.data) ser.IsValid(raise_exception=True) - try: - user_bytes = UUIDObj(ser.ValidatedData['UserUUID']).bytes - pw_obj = Password.objects.get(UserUUID=user_bytes) - valid = bcrypt.checkpw(ser.ValidatedData['Password'].encode(), pw_obj.Password.encode()) - return Response({'code': 0, 'valid': valid}) - except (Password.DoesNotExist, ValueError): + from gvsdsdk.auth.managers import AdminUserManager + found, valid = AdminUserManager.VerifyPassword( + ser.ValidatedData['UserUUID'], + ser.ValidatedData['Password'], + ) + if not found: return Response({'code': 1, 'msg': '用户不存在'}, status=404) + return Response({'code': 0, 'valid': valid}) class SiteUserEnableView(ELTAuthenticatedAPIView): @@ -93,14 +78,11 @@ class SiteUserEnableView(ELTAuthenticatedAPIView): def post(self, request): ser = SiteUserEnableSerializer(data=request.data) ser.IsValid(raise_exception=True) - try: - user_bytes = UUIDObj(ser.ValidatedData['UserUUID']).bytes - user = User.objects.get(UserUUID=user_bytes) - user.UserAccountLicense = 1 - user.save(update_fields=['UserAccountLicense']) - return Response({'code': 0, 'msg': '账户已启用'}) - except (User.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '用户不存在'}, status=404) + from gvsdsdk.auth.managers import AdminUserManager + ok, msg = AdminUserManager.SetAccountLicense(ser.ValidatedData['UserUUID'], 1) + if not ok: + return Response({'code': 1, 'msg': msg}, status=404) + return Response({'code': 0, 'msg': '账户已启用'}) class SiteUserDisableView(ELTAuthenticatedAPIView): @@ -109,14 +91,11 @@ class SiteUserDisableView(ELTAuthenticatedAPIView): def post(self, request): ser = SiteUserDisableSerializer(data=request.data) ser.IsValid(raise_exception=True) - try: - user_bytes = UUIDObj(ser.ValidatedData['UserUUID']).bytes - user = User.objects.get(UserUUID=user_bytes) - user.UserAccountLicense = 0 - user.save(update_fields=['UserAccountLicense']) - return Response({'code': 0, 'msg': '账户已禁用'}) - except (User.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '用户不存在'}, status=404) + from gvsdsdk.auth.managers import AdminUserManager + ok, msg = AdminUserManager.SetAccountLicense(ser.ValidatedData['UserUUID'], 0) + if not ok: + return Response({'code': 1, 'msg': msg}, status=404) + return Response({'code': 0, 'msg': '账户已禁用'}) class SiteUserEmailVerifyView(ELTAuthenticatedAPIView): @@ -125,14 +104,14 @@ class SiteUserEmailVerifyView(ELTAuthenticatedAPIView): def post(self, request): ser = SiteUserEmailVerifySerializer(data=request.data) ser.IsValid(raise_exception=True) - try: - user_bytes = UUIDObj(ser.ValidatedData['UserUUID']).bytes - user = User.objects.get(UserUUID=user_bytes) - user.UserEmail = ser.ValidatedData['Email'] - user.save(update_fields=['UserEmail']) - return Response({'code': 0, 'msg': '邮箱验证已发送'}) - except (User.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '用户不存在'}, status=404) + from gvsdsdk.auth.managers import AdminUserManager + ok, msg = AdminUserManager.SetUserEmail( + ser.ValidatedData['UserUUID'], + ser.ValidatedData['Email'], + ) + if not ok: + return Response({'code': 1, 'msg': msg}, status=404) + return Response({'code': 0, 'msg': '邮箱验证已发送'}) class SiteUserBackpackView(ELTAuthenticatedAPIView): @@ -214,19 +193,11 @@ class SiteUser2FAInfoView(ELTAuthenticatedAPIView): if not user_uuid_str: return Response({'code': 1, 'msg': '缺少UserUUID'}, status=400) try: - user_bytes = UUIDObj(user_uuid_str).bytes + UUIDObj(user_uuid_str).bytes except ValueError: return Response({'code': 1, 'msg': '无效的UserUUID'}, status=400) - try: - two_fa = User2FA.objects.get(UserUUID=user_bytes) - return Response({ - 'code': 0, - 'data': { - '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, - }, - }) - except User2FA.DoesNotExist: + from gvsdsdk.auth.managers import AdminUserManager + info = AdminUserManager.Get2FAInfo(user_uuid_str) + if info is None: return Response({'code': 0, 'data': None, 'msg': '该用户未配置2FA'}) + return Response({'code': 0, 'data': info}) diff --git a/gvsds/apps/gerp/views/user_detail.py b/gvsds/apps/gerp/views/user_detail.py index 8696e068..d566926a 100644 --- a/gvsds/apps/gerp/views/user_detail.py +++ b/gvsds/apps/gerp/views/user_detail.py @@ -40,116 +40,38 @@ class UserDetailView(ELTAuthenticatedAPIView): return self._query_sub(server_name, user_uuid_str) def _query_master(self, user_uuid_str): - """主服务器:直接 ORM 查询""" - from gvsds.apps.core.models import User - from gvsds.apps.gerp.models import UserRole, Role, RolePermission, Permission + """主服务器:经 gvsdsdk UserService 聚合查询""" + from gvsdsdk.auth.user_service import UserService + from gvsds.apps.gerp.models import Company try: user_uuid = UUIDObj(user_uuid_str).bytes except Exception: return Response({'code': 1, 'msg': '无效的 UserUUID'}) - # 用户基本信息 - try: - user = User.objects.get(UserUUID=user_uuid) - except User.DoesNotExist: + detail = UserService.GetUserDetail(user_uuid) + if detail is None: return Response({'code': 1, 'msg': '用户不存在'}) - user_info = { - 'UserUUID': user_uuid_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, - } - - # 角色列表(通过 UserRole.CompanyUUID 获取关联公司) - from gvsdsdk.auth.managers import RoleManager - roles = [] - user_roles = RoleManager.GetUserRoleDetails(user_uuid) - for ur in user_roles: - role = RoleManager.GetRoleByUUID(ur.RoleUUID) - if role: - company_name = '' - company_short = '' - if ur.CompanyUUID: - from gvsds.apps.gerp.models import Company - company = Company.objects.filter(CompanyUUID=ur.CompanyUUID).first() + # 补全角色关联公司名称(SDK 返回 CompanyUUID,公司名称属 SAAS 侧数据) + for role_item in detail['Roles']: + company_uuid_str = role_item.get('CompanyUUID') or '' + company_name = '' + company_short = '' + if company_uuid_str: + try: + company = Company.objects.filter( + CompanyUUID=UUIDObj(company_uuid_str).bytes, + ).first() if company: company_name = company.CompanyName company_short = company.CompanyShort or '' - roles.append({ - 'RoleUUID': str(UUIDObj(role.RoleUUID)) if role.RoleUUID else '', - 'RoleName': role.RoleName, - 'RoleType': role.RoleType, - 'RoleStatus': role.RoleStatus, - 'AssignScope': role.AssignScope, - 'RoleDesc': role.RoleDesc or '', - 'CompanyName': company_name, - 'CompanyShort': company_short, - }) + except Exception: + pass + role_item['CompanyName'] = company_name + role_item['CompanyShort'] = company_short - # 权限列表(通过 UserRole → RolePermission → Permission 链路查询,按公司过滤) - permissions = [] - role_uuids = [ur.RoleUUID for ur in user_roles if ur.RoleUUID] - company_uuids = [ur.CompanyUUID for ur in user_roles if ur.CompanyUUID] - if role_uuids: - rp_qs = RolePermission.objects.filter( - RoleUUID__in=role_uuids - ) - if company_uuids: - rp_qs = rp_qs.filter(CompanyUUID__in=company_uuids) - perm_uuids = rp_qs.values_list('PermUUID', flat=True) - perms = Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) - seen = set() - for p in perms: - key = p.PermCode - if key not in seen: - seen.add(key) - permissions.append({ - 'PermName': p.PermName, - 'PermCode': p.PermCode, - 'PermCategory': p.PermCategory or '', - 'PermDesc': p.PermDesc or '', - }) - - # 订单列表(最近 20 条) - orders = [] - try: - from gvsdsdk.order.models import Order - user_orders = Order.objects.filter( - BuyerUserUUID=user_uuid - ).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 # 订单表可能不存在 - - return Response({ - 'code': 0, - 'data': { - 'User': user_info, - 'Roles': roles, - 'Permissions': permissions, - 'Orders': orders, - } - }) + return Response({'code': 0, 'data': detail}) def _query_sub(self, server_name, user_uuid_str): """子服务器:通过 form proxy 查询 User 表""" diff --git a/gvsdsdk/minecraft/auth.py b/gvsds/apps/mc/manager.py similarity index 91% rename from gvsdsdk/minecraft/auth.py rename to gvsds/apps/mc/manager.py index 90d716f8..3778150e 100644 --- a/gvsdsdk/minecraft/auth.py +++ b/gvsds/apps/mc/manager.py @@ -1,5 +1,6 @@ -"""模块:gvsdsdk.minecraft.auth — EasyAuth 插件认证与绑定管理 +"""模块:gvsds.apps.mc.manager — MC 定制业务(EasyAuth 认证与绑定管理) +MC 相关为定制业务,自 gvsdsdk 迁回 gvsds 主工程(原 gvsdsdk.minecraft.auth)。 提供: - ``VerifyPassword``:使用 bcrypt 校验游戏账号密码 - ``EasyAuthManager``:游戏账号查询、密码验证、绑定标记读写 @@ -7,6 +8,11 @@ import bcrypt +from gvsdsdk.models import UserRole +from gvsdsdk.model_utils import UUIDObj + +from gvsds.apps.mc.models import EasyAuth + def VerifyPassword(plain_password: str, bcrypt_hash: str) -> bool: """校验明文密码与 bcrypt hash 是否匹配 @@ -39,7 +45,6 @@ class EasyAuthManager: Returns: ``EasyAuth`` 实例;不存在则返回 ``None`` """ - from .models import EasyAuth if not username: return None return EasyAuth.objects.filter(UsernameLower=username.lower()).first() @@ -79,7 +84,6 @@ class EasyAuthManager: Returns: 实际更新行数:1 成功,0 表示已被其他请求抢先绑定 """ - from .models import EasyAuth if easyauth is None: return 0 return EasyAuth.objects.filter( @@ -90,7 +94,6 @@ class EasyAuthManager: @staticmethod def ClearBound(easyauth) -> int: """清空 easyauth 的 UserRoleUUID(用于解绑场景)""" - from .models import EasyAuth if easyauth is None: return 0 return EasyAuth.objects.filter( @@ -109,14 +112,11 @@ class EasyAuthManager: 因此反过来即可定位该 GVSDS 用户绑定的 MC 玩家。 Args: - gvsds_user_uuid: GVDS 用户 UUID(bytes/str/UUIDObj 均可) + gvsds_user_uuid: GVSDS 用户 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 @@ -140,7 +140,6 @@ class EasyAuthManager: Returns: ``EasyAuth`` 实例;不存在或入参为空则返回 ``None`` """ - from .models import EasyAuth if not player_uuid: return None return EasyAuth.objects.filter(PlayerUUID=player_uuid).first() diff --git a/gvsds/apps/mc/models.py b/gvsds/apps/mc/models.py index 99fbe2e5..257b724c 100644 --- a/gvsds/apps/mc/models.py +++ b/gvsds/apps/mc/models.py @@ -3,18 +3,47 @@ Minecraft 游戏用户映射与 EasyAuth 插件认证表。 - ``EasyAuth``:MC EasyAuth 插件认证表(managed=False,外部 schema) - —— 自 gvsdsdk.minecraft.models 导入,供本 app 复用同一模型类定义 + —— 模型定义原在 gvsdsdk.minecraft.models(MC 为定制业务,已迁回主工程) EasyAuth 表的 ``data`` 列为 JSON,包含 ``password``(bcrypt hash)、 ``last_ip``、``registration_date`` 等字段。新增 ``UserRoleUUID`` 列 (BINARY(16))存绑定后创建的 UserRole 关联 UUID,用于 MC 插件侧反查 GVSDS 身份。 注:原 MCUser 表(mc_users,一对一映射)已废弃删除——GVSDS 用户与 MC 玩家 -的多对多绑定改走 UserRole + easyauth.UserRoleUUID。""" +的多对多绑定改走 UserRole + easyauth.UserRoleUUID。 -# EasyAuth 模型统一定义在 gvsdsdk.minecraft.models(app_label='mc', -# 通过 gvsds/db_router.py 路由到 minecraft 库)。 -# 此处仅 import 子模块以触发 EasyAuth 类创建并注册到 mc app,**不** 在本 -# 模块命名空间引入 EasyAuth 名字——否则与 gvsdsdk.minecraft.models 中 -# 已注册的同名类触发 Django "Conflicting models" RuntimeError。 -import gvsdsdk.minecraft.models # noqa: F401 +模型 app_label='mc',通过 gvsds/db_router.py 路由到 minecraft 库。""" + +from django.db import models + +from gvsdsdk.model_base import QModel +from gvsdsdk.models import UUIDField + + +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 diff --git a/gvsds/apps/mc/views.py b/gvsds/apps/mc/views.py index 4caccaa1..65d1b2a8 100644 --- a/gvsds/apps/mc/views.py +++ b/gvsds/apps/mc/views.py @@ -13,7 +13,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from gvsdsdk.minecraft import EasyAuthManager +from gvsds.apps.mc.manager import EasyAuthManager from gvsdsdk.auth import RoleManager from gvsds.apps.gerp.models import Company @@ -83,7 +83,7 @@ class UserBindView(APIView): # 允许一个用户绑定多个不同玩家(多对多),但不允许重复绑定同一玩家。 from gvsdsdk.models import UserRole as _UR from gvsdsdk.model_utils import UUIDObj as _UUIDObj - from gvsdsdk.minecraft.models import EasyAuth as _EasyAuthModel + from gvsds.apps.mc.models import EasyAuth as _EasyAuthModel _user_role_uuids = list( _UR.objects.filter(UserUUID=_UUIDObj(gvsds_user_uuid).bytes) .values_list('UserRoleUUID', flat=True) diff --git a/gvsds/apps/ugm/views.py b/gvsds/apps/ugm/views.py index de5c1453..af64c98a 100644 --- a/gvsds/apps/ugm/views.py +++ b/gvsds/apps/ugm/views.py @@ -1,8 +1,5 @@ import re -import uuid -import datetime import io -import bcrypt from django.http import HttpResponse, HttpResponseRedirect from django.core.cache import cache from django.conf import settings @@ -10,12 +7,9 @@ from rest_framework import status from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny, IsAuthenticated -from rest_framework.authentication import TokenAuthentication from rest_framework_simplejwt.tokens import RefreshToken from PIL import Image -from gvsds.apps.core.models import User, User2FA, UserAvatar -from gvsds.apps.gerp.models import Password from gvsdsdk.model_utils import UUIDObj from gvsds.app_config import getlist as _app_getlist from .serializers import ( @@ -113,9 +107,9 @@ class LoginView(APIView): username = serializer.ValidatedData['UserName'] password = serializer.ValidatedData['UserPassword'] - try: - user = User.objects.get(UserName=username) - except User.DoesNotExist: + from gvsdsdk.auth.managers import AuthManager + user = AuthManager.GetUserByUserName(username) + if user is None: return Response({'code': 1, 'msg': '用户名或密码错误'}, status=401) if not user.CheckPassword(password): @@ -124,22 +118,18 @@ class LoginView(APIView): if user.UserAccountLicense != 1: return Response({'code': 1, 'msg': '账户已被禁用'}, status=403) - try: - two_fa = User2FA.objects.get(UserUUID=user.UserUUID) - if two_fa.IsEnabled == 1: - request.session['pending_2fa_user'] = UUIDObj(user.UserUUID).str - return Response({ - 'code': 2, - 'msg': '需要2FA验证', - 'Require2FA': True, - 'PendingKey': UUIDObj(user.UserUUID).str, - }) - except User2FA.DoesNotExist: - pass + requires_2fa, _ = AuthManager.Check2FARequired(user.UserUUID) + if requires_2fa: + request.session['pending_2fa_user'] = UUIDObj(user.UserUUID).str + return Response({ + 'code': 2, + 'msg': '需要2FA验证', + 'Require2FA': True, + 'PendingKey': UUIDObj(user.UserUUID).str, + }) elt = _IssueJwtTokens(user) - user.UserLastLoginDate = datetime.datetime.utcnow() - user.save(update_fields=['UserLastLoginDate']) + AuthManager.UpdateLastLogin(user.UserUUID) response = Response({ 'code': 0, @@ -172,18 +162,11 @@ class RegisterView(APIView): password = serializer.ValidatedData['UserPassword'] email = serializer.ValidatedData['UserEmail'] - if User.objects.filter(UserName=username).exists(): + from gvsdsdk.auth.managers import AuthManager + if AuthManager.IsUserNameTaken(username): return Response({'code': 1, 'msg': '用户名已存在'}, status=409) - user = User( - UserUUID=UUIDObj(uuid.uuid4()), - UserName=username, - UserEmail=email, - UserAccountLicense=1, - UserPositionStatus=1, - ) - user.SetPassword(password) - user.save() + user = AuthManager.CreateUser(username, email, password) # 为新用户创建默认角色并绑定到系统公司(GERP 后台) from gvsds.apps.gerp.form_service import create_default_role_for_user @@ -191,8 +174,7 @@ class RegisterView(APIView): # 注册后自动登录:发放 JWT token elt = _IssueJwtTokens(user) - user.UserLastLoginDate = datetime.datetime.utcnow() - user.save(update_fields=['UserLastLoginDate']) + AuthManager.UpdateLastLogin(user.UserUUID) response = Response({ 'code': 0, @@ -242,14 +224,11 @@ class Verify2FAView(APIView): return Response({'code': 1, 'msg': '验证尝试次数过多,请15分钟后再试'}, status=429) code = serializer.ValidatedData['Code'] - try: - two_fa = User2FA.objects.get(UserUUID=UUIDObj(pending_user_uuid).bytes) - except User2FA.DoesNotExist: + from gvsdsdk.auth.managers import AuthManager + enabled, _ = AuthManager.Check2FARequired(pending_user_uuid) + if not enabled: return Response({'code': 1, 'msg': '2FA未设置'}, status=400) - - import pyotp - totp = pyotp.TOTP(two_fa.SecretKey) - if not totp.verify(code, valid_window=1): + if not AuthManager.Verify2FA(pending_user_uuid, code): # 验证失败,递增计数器 cache.set(rate_limit_key, attempts + 1, self.BLOCK_DURATION_SECONDS) return Response({'code': 1, 'msg': '验证码错误'}, status=400) @@ -259,7 +238,7 @@ class Verify2FAView(APIView): del request.session['pending_2fa_user'] - user = User.objects.get(UserUUID=UUIDObj(pending_user_uuid).bytes) + user = AuthManager.GetUserByUUID(pending_user_uuid) elt = _IssueJwtTokens(user) response = Response({ @@ -360,16 +339,15 @@ class GetUserAvatarView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - UserUUID = request.user.UserUUID - try: - avatar = UserAvatar.objects.get(UserUUID=UserUUID) - content_type = f'image/{avatar.AvatarFileType}' - return HttpResponse(avatar.UserAvatar, content_type=content_type) - except UserAvatar.DoesNotExist: - default = Image.new('RGB', (256, 256), (200, 200, 200)) - buf = io.BytesIO() - default.save(buf, format='PNG') - return HttpResponse(buf.getvalue(), content_type='image/png') + from gvsdsdk.auth.managers import AuthManager + avatar_data = AuthManager.GetAvatar(request.user.UserUUID) + if avatar_data is not None: + avatar_bytes, file_type = avatar_data + return HttpResponse(avatar_bytes, content_type=f'image/{file_type}') + default = Image.new('RGB', (256, 256), (200, 200, 200)) + buf = io.BytesIO() + default.save(buf, format='PNG') + return HttpResponse(buf.getvalue(), content_type='image/png') class GetArgsUserAvatarView(APIView): @@ -382,15 +360,15 @@ class GetArgsUserAvatarView(APIView): UserUUID = UUIDObj(user_uuid_str).bytes if not UserUUID: return Response({'code': 1, 'msg': '无效的UserUUID'}, status=400) - try: - avatar = UserAvatar.objects.get(UserUUID=UserUUID) - content_type = f'image/{avatar.AvatarFileType}' - return HttpResponse(avatar.UserAvatar, content_type=content_type) - except UserAvatar.DoesNotExist: - default = Image.new('RGB', (256, 256), (200, 200, 200)) - buf = io.BytesIO() - default.save(buf, format='PNG') - return HttpResponse(buf.getvalue(), content_type='image/png') + from gvsdsdk.auth.managers import AuthManager + avatar_data = AuthManager.GetAvatar(UserUUID) + if avatar_data is not None: + avatar_bytes, file_type = avatar_data + return HttpResponse(avatar_bytes, content_type=f'image/{file_type}') + default = Image.new('RGB', (256, 256), (200, 200, 200)) + buf = io.BytesIO() + default.save(buf, format='PNG') + return HttpResponse(buf.getvalue(), content_type='image/png') class GetUserNameView(APIView): @@ -407,12 +385,14 @@ class ModifyPasswordView(APIView): serializer = ModifyPasswordSerializer(data=request.data) serializer.IsValid(raise_exception=True) - user = request.user - if not user.CheckPassword(serializer.ValidatedData['OldPassword']): - return Response({'code': 1, 'msg': '原密码错误'}, status=400) - - user.SetPassword(serializer.ValidatedData['NewPassword']) - user.save(update_fields=['UserPassword']) + from gvsdsdk.auth.managers import AuthManager + ok, msg = AuthManager.ChangePassword( + request.user.UserUUID, + serializer.ValidatedData['OldPassword'], + serializer.ValidatedData['NewPassword'], + ) + if not ok: + return Response({'code': 1, 'msg': msg}, status=400) return Response({'code': 0, 'msg': '密码修改成功'}) @@ -442,18 +422,8 @@ class ModifyAvatarView(APIView): return Response({'code': 1, 'msg': '图片处理失败'}, status=400) detected_type = 'png' if save_format == 'PNG' else 'jpeg' - UserUUID = request.user.UserUUID - try: - avatar = UserAvatar.objects.get(UserUUID=UserUUID) - avatar.UserAvatar = avatar_bytes - avatar.AvatarFileType = detected_type - avatar.save(update_fields=['UserAvatar', 'AvatarFileType', 'AvatarUploadTime']) - except UserAvatar.DoesNotExist: - UserAvatar.objects.create( - UserUUID=UserUUID, - UserAvatar=avatar_bytes, - AvatarFileType=detected_type, - ) + from gvsdsdk.auth.managers import AuthManager + AuthManager.SetAvatar(request.user.UserUUID, avatar_bytes, detected_type) return Response({'code': 0, 'msg': '头像修改成功'}) diff --git a/gvsdsdk/__init__.py b/gvsdsdk/__init__.py index c78c9d5f..c92f0dcd 100644 --- a/gvsdsdk/__init__.py +++ b/gvsdsdk/__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/gvsdsdk/auth/__init__.py b/gvsdsdk/auth/__init__.py index 00ca8ea6..51116737 100644 --- a/gvsdsdk/auth/__init__.py +++ b/gvsdsdk/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/gvsdsdk/auth/managers.py b/gvsdsdk/auth/managers.py index e45dcd39..7141b2ed 100644 --- a/gvsdsdk/auth/managers.py +++ b/gvsdsdk/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: """职务管理器——提供人事职务标签创建、岗位模板管理""" @@ -332,3 +396,375 @@ class PositionManager: ).values_list('PermUUID', flat=True) return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) + @staticmethod + def HasTemplatePerm(TemplateUUID, PermUUID): + """检查岗位模板是否已关联指定权限""" + return PositionTemplatePermission.objects.filter( + 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/gvsdsdk/commission/manager.py b/gvsdsdk/commission/manager.py index 6fe2189d..8c5fc1a5 100644 --- a/gvsdsdk/commission/manager.py +++ b/gvsdsdk/commission/manager.py @@ -166,6 +166,54 @@ class CommissionEngine: logger.info(f'费率表已创建: {table_code}@tenant={self.TenantUUID}') return table + def ListRules(self, event_type=None): + """列出租户下的分佣规则(仅启用,按优先级降序) + + Args: + event_type: 可选,按事件类型过滤 + + Returns: + QuerySet[CommissionRule] + """ + qs = CommissionRule.objects.filter( + TenantUUID=self.TenantUUID, RuleStatus=1, + ) + if event_type: + qs = qs.filter(EventType=event_type) + return qs.order_by('-RulePriority') + + def DisableRule(self, rule_uuid): + """软禁用分佣规则(RuleStatus=0);返回是否命中""" + updated = CommissionRule.objects.filter( + TenantUUID=self.TenantUUID, RuleUUID=rule_uuid, + ).update(RuleStatus=0) + return updated > 0 + + def ListRateTables(self): + """列出租户下的费率表(仅启用) + + Returns: + QuerySet[RateTable] + """ + return RateTable.objects.filter( + TenantUUID=self.TenantUUID, TableStatus=1, + ) + + def ListEvents(self, event_type=None, limit=50): + """列出租户下的分佣事件(按创建时间倒序) + + Args: + event_type: 可选,按事件类型过滤 + limit: 返回条数上限 + + Returns: + QuerySet[CommissionEvent] + """ + qs = CommissionEvent.objects.filter(TenantUUID=self.TenantUUID) + if event_type: + qs = qs.filter(EventType=event_type) + return qs.order_by('-CreateTime')[:limit] + def Dispatch(self, event_type, event_key, total_amount, source_user_uuid=None, participants=None, tier_index=0, payload=None, auto_settle=True): diff --git a/gvsdsdk/minecraft/__init__.py b/gvsdsdk/minecraft/__init__.py deleted file mode 100644 index d54ebd47..00000000 --- a/gvsdsdk/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/gvsdsdk/minecraft/models.py b/gvsdsdk/minecraft/models.py deleted file mode 100644 index 86a3f2d7..00000000 --- a/gvsdsdk/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 diff --git a/gvsdsdk/payment/manager.py b/gvsdsdk/payment/manager.py index 8f45ed95..6527de64 100644 --- a/gvsdsdk/payment/manager.py +++ b/gvsdsdk/payment/manager.py @@ -160,6 +160,49 @@ class PaymentManager: logger.info(f'支付网关配置: {platform}@tenant={self.TenantUUID}') return config + def ListConfigs(self, status=None): + """列出租户下的支付网关配置(默认按默认渠道优先、平台名排序) + + Args: + status: 可选,按配置状态过滤(1=启用,0=禁用) + + Returns: + QuerySet[PaymentGatewayConfig] + """ + qs = PaymentGatewayConfig.objects.filter( + TenantUUID=self.TenantUUID, + ) + if status is not None: + qs = qs.filter(ConfigStatus=status) + return qs.order_by('-ConfigDefault', 'Platform') + + def GetConfigByUUID(self, config_uuid): + """按 ConfigUUID 查询租户下的配置(不存在返回 None)""" + return PaymentGatewayConfig.objects.filter( + ConfigUUID=config_uuid, TenantUUID=self.TenantUUID, + ).first() + + def DisableConfig(self, platform): + """禁用租户指定平台的配置(ConfigStatus=0 并清空更新时间);返回是否命中""" + updated = PaymentGatewayConfig.objects.filter( + TenantUUID=self.TenantUUID, Platform=platform, + ).update(ConfigStatus=0, UpdateTime=None) + return updated > 0 + + def SetConfigStatus(self, config_uuid, status): + """更新租户指定配置的状态;返回是否命中""" + updated = PaymentGatewayConfig.objects.filter( + ConfigUUID=config_uuid, TenantUUID=self.TenantUUID, + ).update(ConfigStatus=status) + return updated > 0 + + def DeleteConfig(self, config_uuid): + """删除租户指定配置;返回是否命中""" + deleted, _ = PaymentGatewayConfig.objects.filter( + ConfigUUID=config_uuid, TenantUUID=self.TenantUUID, + ).delete() + return deleted > 0 + def Pay(self, platform, amount, subject='', body='', method='jsapi', open_id='', client_ip='', extra=None, expire_minutes=30):