diff --git a/__init__.py b/__init__.py index b24ecbc..c78c9d5 100644 --- a/__init__.py +++ b/__init__.py @@ -13,11 +13,10 @@ GVSDS 分布式服务间通信 SDK,作为子服务器(如 ATestCServer)和 包结构: gvsdsdk/ ├── auth/ # 用户认证与权限模块 - │ ├── cookie.py # 默认头像等工具 │ ├── crypto.py # 密码哈希 + AES 加密 │ ├── middleware.py # CurrentRoleMiddleware │ ├── permissions.py # RBAC/ABAC 权限 - │ └── managers.py # UserManager + RoleManager + │ └── managers.py # RoleManager(UserManager 在 models.py) ├── msync/ # MSYNC 服务间通信 ├── commission/ # 分佣引擎 │ ├── models.py # 分佣数据模型 diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc index d4782ed..76d35aa 100644 Binary files a/__pycache__/__init__.cpython-312.pyc and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/db_router.cpython-312.pyc b/__pycache__/db_router.cpython-312.pyc index a7c9180..6da9796 100644 Binary files a/__pycache__/db_router.cpython-312.pyc and b/__pycache__/db_router.cpython-312.pyc differ diff --git a/__pycache__/form_service.cpython-312.pyc b/__pycache__/form_service.cpython-312.pyc index a8e392b..48f5de5 100644 Binary files a/__pycache__/form_service.cpython-312.pyc and b/__pycache__/form_service.cpython-312.pyc differ diff --git a/__pycache__/model_base.cpython-312.pyc b/__pycache__/model_base.cpython-312.pyc index 6cf5b56..1f0691e 100644 Binary files a/__pycache__/model_base.cpython-312.pyc and b/__pycache__/model_base.cpython-312.pyc differ diff --git a/__pycache__/models.cpython-312.pyc b/__pycache__/models.cpython-312.pyc index 1570c2b..37041c8 100644 Binary files a/__pycache__/models.cpython-312.pyc and b/__pycache__/models.cpython-312.pyc differ diff --git a/auth/__init__.py b/auth/__init__.py index 69db92b..00ca8ea 100644 --- a/auth/__init__.py +++ b/auth/__init__.py @@ -1,21 +1,16 @@ """gvsdsdk.auth — 用户认证与权限模块""" __all__ = [ - 'GetDefaultAvatar', 'HashPassword', 'VerifyPassword', 'SymmetricEncrypt', 'SymmetricDecrypt', 'FileSHA1', 'FileSHA256', 'BytesSHA1', 'BytesSHA256', 'CurrentRoleMiddleware', 'IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'TenantScopedMixin', 'UserScopedMixin', 'PermissionVerifier', 'IsAdminScopeDisabled', 'IsTenantScopeDisabled', 'IsAdmin', 'IsSuperAdmin', 'IsTenantAdmin', - 'RoleManager', 'ShopManager', 'PositionManager', + 'RoleManager', ] def __getattr__(name): - if name == 'GetDefaultAvatar': - from gvsdsdk.auth.cookie import GetDefaultAvatar - globals()['GetDefaultAvatar'] = GetDefaultAvatar - return GetDefaultAvatar if name in ('HashPassword', 'VerifyPassword', 'SymmetricEncrypt', 'SymmetricDecrypt', 'FileSHA1', 'FileSHA256', 'BytesSHA1', 'BytesSHA256'): from gvsdsdk.auth import crypto as _m val = getattr(_m, name) @@ -39,12 +34,4 @@ def __getattr__(name): from gvsdsdk.auth.managers import RoleManager globals()['RoleManager'] = RoleManager return RoleManager - if name == 'ShopManager': - from gvsdsdk.auth.managers import ShopManager - globals()['ShopManager'] = ShopManager - return ShopManager - if name == 'PositionManager': - from gvsdsdk.auth.managers import PositionManager - globals()['PositionManager'] = PositionManager - return PositionManager 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 113e3b6..e8b2160 100644 Binary files a/auth/__pycache__/__init__.cpython-312.pyc and b/auth/__pycache__/__init__.cpython-312.pyc differ diff --git a/auth/__pycache__/elt_auth.cpython-312.pyc b/auth/__pycache__/elt_auth.cpython-312.pyc deleted file mode 100644 index d3fd43f..0000000 Binary files a/auth/__pycache__/elt_auth.cpython-312.pyc and /dev/null differ diff --git a/auth/__pycache__/managers.cpython-312.pyc b/auth/__pycache__/managers.cpython-312.pyc index e8e1690..5d42386 100644 Binary files a/auth/__pycache__/managers.cpython-312.pyc and b/auth/__pycache__/managers.cpython-312.pyc differ diff --git a/auth/cookie.py b/auth/cookie.py deleted file mode 100644 index 0052c3d..0000000 --- a/auth/cookie.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.http import HttpResponse - - -def GetDefaultAvatar(): - from io import BytesIO - from PIL import Image - img = Image.new('RGB', (256, 256), (200, 200, 200)) - buf = BytesIO() - img.save(buf, format='PNG') - buf.seek(0) - return HttpResponse(buf.read(), content_type='image/png') diff --git a/auth/managers.py b/auth/managers.py index e310514..8267fef 100644 --- a/auth/managers.py +++ b/auth/managers.py @@ -1,11 +1,10 @@ import uuid from django.utils import timezone as _timezone -from gvsdsdk.models import Role, UserRole, RolePermission, Permission, Shop, RoleShop, Position, PositionTemplate, PositionTemplatePermission +from gvsdsdk.models import Role, UserRole, RolePermission, Permission from gvsdsdk.model_utils import _GetUserModel -# NOTE: 类型注解(L8)暂缓——本模块含 RoleManager/ShopManager/PositionManager 共 20+ 静态方法, -# 全量补充参数与返回值注解工作量较大且易引入回归,后续单独迭代处理。 +# NOTE: 类型注解暂缓——RoleManager 静态方法较多,全量补充注解工作量较大且易引入回归,后续单独迭代处理。 class RoleManager: @@ -139,151 +138,3 @@ class RoleManager: perm_uuids = qs.values_list('PermUUID', flat=True) return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) - -class ShopManager: - """商铺管理器——提供商铺创建、角色关联、查询""" - - @staticmethod - def CreateShop(Name, CompanyUUID, Code='', ShopType=0, **Kwargs): - """创建商铺""" - shop = Shop( - ShopUUID=uuid.uuid4().bytes, - CompanyUUID=CompanyUUID, - ShopName=Name, - ShopCode=Code, - ShopType=ShopType, - ShopStatus=1, - ShopAddress=Kwargs.get('ShopAddress', ''), - ShopContact=Kwargs.get('ShopContact', ''), - ShopPhone=Kwargs.get('ShopPhone', ''), - ShopDesc=Kwargs.get('ShopDesc', ''), - CreateTime=_timezone.now(), - UpdateTime=_timezone.now(), - ) - shop.save() - return shop - - @staticmethod - def AssignRole(RoleUUID, ShopUUID, IsMainShop=False): - """为角色关联商铺""" - role_shop = RoleShop( - RoleShopUUID=uuid.uuid4().bytes, - RoleUUID=RoleUUID, - ShopUUID=ShopUUID, - IsMainShop=1 if IsMainShop else 0, - CreateTime=_timezone.now(), - ) - role_shop.save() - return role_shop - - @staticmethod - def RemoveRole(RoleUUID, ShopUUID): - """移除角色与商铺的关联""" - deleted, _ = RoleShop.objects.filter(RoleUUID=RoleUUID, ShopUUID=ShopUUID).delete() - return deleted > 0 - - @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 GetRolesForShop(ShopUUID): - """获取商铺关联的所有角色""" - role_uuids = RoleShop.objects.filter(ShopUUID=ShopUUID).values_list('RoleUUID', flat=True) - return Role.objects.filter(RoleUUID__in=role_uuids) - - @staticmethod - def GetMainShopForRole(RoleUUID): - """获取角色的主商铺""" - rs = RoleShop.objects.filter(RoleUUID=RoleUUID, IsMainShop=1).first() - if rs: - return Shop.objects.filter(ShopUUID=rs.ShopUUID).first() - return None - - @staticmethod - def GetShopsForCompany(CompanyUUID): - """获取公司下的所有商铺""" - return Shop.objects.filter(CompanyUUID=CompanyUUID, ShopStatus=1) - - -class PositionManager: - """职务管理器——提供人事职务标签创建、岗位模板管理""" - - @staticmethod - def CreatePosition(Name, TenantUUID, Code='', Level=0, **Kwargs): - """创建人事职务标签""" - position = Position( - PositionUUID=uuid.uuid4().bytes, - TenantUUID=TenantUUID, - PositionName=Name, - PositionCode=Code, - PositionLevel=Level, - PositionStatus=1, - PositionDesc=Kwargs.get('PositionDesc', ''), - CreateTime=_timezone.now(), - UpdateTime=_timezone.now(), - ) - position.save() - return position - - @staticmethod - def GetPositionsForTenant(TenantUUID): - """获取租户下的所有职务""" - return Position.objects.filter(TenantUUID=TenantUUID, PositionStatus=1) - - @staticmethod - def CreatePositionTemplate(Name, TenantUUID, Code='', **Kwargs): - """创建岗位模板""" - template = PositionTemplate( - PositionTemplateUUID=uuid.uuid4().bytes, - TenantUUID=TenantUUID, - TemplateName=Name, - TemplateCode=Code, - TemplateStatus=1, - TemplateDesc=Kwargs.get('TemplateDesc', ''), - CreateTime=_timezone.now(), - UpdateTime=_timezone.now(), - ) - template.save() - return template - - @staticmethod - def AssignPermToTemplate(TemplateUUID, PermUUID): - """为岗位模板关联权限""" - ptp = PositionTemplatePermission( - PTPermUUID=uuid.uuid4().bytes, - PositionTemplateUUID=TemplateUUID, - PermUUID=PermUUID, - CreateTime=_timezone.now(), - ) - ptp.save() - return ptp - - @staticmethod - def RemovePermFromTemplate(TemplateUUID, PermUUID): - """移除岗位模板与权限的关联""" - deleted, _ = PositionTemplatePermission.objects.filter( - PositionTemplateUUID=TemplateUUID, PermUUID=PermUUID - ).delete() - return deleted > 0 - - @staticmethod - def GetPermsForTemplate(TemplateUUID): - """获取岗位模板关联的所有权限""" - perm_uuids = PositionTemplatePermission.objects.filter( - PositionTemplateUUID=TemplateUUID - ).values_list('PermUUID', flat=True) - return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) - - @staticmethod - def GetTemplatesForTenant(TenantUUID): - """获取租户下的所有岗位模板""" - return PositionTemplate.objects.filter(TenantUUID=TenantUUID, TemplateStatus=1) - - @staticmethod - def CopyTemplateToRole(TemplateUUID, RoleName, TenantUUID): - """从岗位模板复制权限到新角色""" - template = PositionTemplate.objects.get(PositionTemplateUUID=TemplateUUID) - return template.CopyToRole(RoleName, TenantUUID) diff --git a/db_router.py b/db_router.py index b74abed..accf15f 100644 --- a/db_router.py +++ b/db_router.py @@ -35,14 +35,8 @@ class SAASDatabaseRouter: GVSDSDK_SAAS_READ_ONLY = True # 默认 True(子服务器只读) """ - # gvsdsdk 内所有模块前缀 - _GVSDSDK_MODULES = frozenset({ - 'gvsdsdk.models', - 'gvsdsdk.commission.models', - 'gvsdsdk.payment.models', - }) - - # gvsdsdk 内所有 app_label + # gvsdsdk 内所有模块前缀(startswith 判定覆盖全部子模块, + # 无需维护显式模块名清单,避免新增模块时遗漏) _GVSDSDK_APP_LABELS = frozenset({ 'gvsdsdk', }) @@ -57,9 +51,9 @@ class SAASDatabaseRouter: def _is_gvsdsdk_model(self, model): """判断模型是否属于 gvsdsdk""" - # 按模块路径判断 + # 按模块路径判断(gvsdsdk.* 前缀全覆盖) module = getattr(model, '__module__', '') - if module in self._GVSDSDK_MODULES or module.startswith('gvsdsdk.'): + if module.startswith('gvsdsdk.'): return True # 按 app_label 判断 app_label = getattr(model._meta, 'app_label', '') diff --git a/form_service.py b/form_service.py index ea2cb09..c4488fd 100644 --- a/form_service.py +++ b/form_service.py @@ -14,9 +14,8 @@ 'MyModel2': MyModel2, } - # 子服务器 views.py - from gvsdsdk.form_service import FormServiceAPIView - from .form_service import FormService + # 子服务器 views.py 中直接实例化 FormService 使用 + # (FormServiceBase 提供 ORM 能力,视图层由子服务器自行封装) """ import uuid @@ -87,6 +86,17 @@ class FormServiceBase: if val is None or val == '': if getattr(field, 'null', False): return None + # 非空字段:数值类型返回默认值或0,布尔类型返回 False,其他类型返回空字符串 + field_type = field.__class__.__name__ + if field_type in ('SmallIntegerField', 'IntegerField', 'BigIntegerField', + 'PositiveIntegerField', 'PositiveSmallIntegerField', + 'FloatField', 'DecimalField'): + default = getattr(field, 'default', None) + if default is not None: + return default + return 0 + if field_type == 'BooleanField': + return False return val field_type = field.__class__.__name__ if field_type == 'UUIDField' and isinstance(val, str): @@ -115,12 +125,25 @@ class FormServiceBase: return val if field_type == 'DateTimeField' and isinstance(val, str): try: - return datetime.datetime.fromisoformat(val) + dt = datetime.datetime.fromisoformat(val) + # USE_TZ=False + MySQL 不接受 aware datetime: + # 统一转 naive UTC(UTC-0 存储,展示时由前端按用户时区转换) + if dt.tzinfo is not None: + dt = dt.astimezone(datetime.timezone.utc).replace(tzinfo=None) + return dt except (ValueError, TypeError): return val - if hasattr(field, 'max_length') and field.max_length is not None and isinstance(val, str) and len(val) > field.max_length: - logger.warning('字段 %s 值超长(%d > %d),已截断', getattr(field, 'name', '?'), len(val), field.max_length) - val = val[:field.max_length] + if field_type in ('BooleanField', 'NullBooleanField') and isinstance(val, str): + low = val.strip().lower() + if low in ('true', '1', 't', 'yes'): + return True + if low in ('false', '0', 'f', 'no'): + return False + return val + max_length = getattr(field, 'max_length', None) + if max_length and isinstance(val, str) and len(val) > max_length: + logger.warning('字段 %s 值超长(%d > %d),已截断', getattr(field, 'name', '?'), len(val), max_length) + val = val[:max_length] return val @staticmethod @@ -141,7 +164,6 @@ class FormServiceBase: 'FloatField': 'number', 'DecimalField': 'number', 'BooleanField': 'boolean', - 'NullBooleanField': 'boolean', 'DateTimeField': 'datetime', 'DateField': 'date', 'TimeField': 'time', diff --git a/model_base.py b/model_base.py index 3324e1c..c522eda 100644 --- a/model_base.py +++ b/model_base.py @@ -24,12 +24,7 @@ from __future__ import annotations import re -from typing import Any, Callable, Dict, Generic, Optional, Tuple, Type, TypeVar, Union - -try: - from typing import Self -except ImportError: - from typing_extensions import Self +from typing import Any, Callable, Dict, Generic, Optional, Self, Tuple, Type, TypeVar, Union from django.db import models from django.db.models import Q diff --git a/models.py b/models.py index 333e618..5fd6ad0 100644 --- a/models.py +++ b/models.py @@ -7,7 +7,7 @@ 供子服务器直接进行跨服务器 ORM 查询。 权限体系四大分层: - 1. 组织人事层:Tenant → Company → Department,员工通过 UserRole.DeptUUID 归属部门 + 1. 组织人事层:Tenant → Company → Department,员工通过 UserRoleDepartment/UserRoleWorkgroup 关联表归属部门/工作组 2. 岗位模板层:PositionTemplate + PositionTemplatePermission,权限初始化快照 3. 角色权限载体层:Role + RolePermission,运行时鉴权实体 4. 鉴权规则层:操作权限校验(角色汇总)+ 数据权限校验(组织属性) @@ -15,11 +15,13 @@ 使用方式(子服务器): from gvsdsdk.models import Permission, Role, UserRole, RolePermission - # 查询所有启用的用户(需在业务子类中定义具体 User 模型) - users = User.objects.filter(UserAccountLicense=1) + # 查询所有启用的用户(User 为抽象基类,需经 _GetUserModel 获取具体模型) + from gvsdsdk.model_utils import _GetUserModel + UserModel = _GetUserModel() + users = UserModel.objects.filter(UserAccountLicense=1) # 或配置 DatabaseRouter 后直接使用 - users = User.objects.filter(UserAccountLicense=1) + users = UserModel.objects.filter(UserAccountLicense=1) 设计原则: 1. 独立:不依赖 gvsds 内部模块,可在任意 Django 项目中独立使用 @@ -29,8 +31,7 @@ 5. 租户隔离:所有权限相关数据依附于独立租户空间,不同租户互相不可见 注意:User 为抽象基类,仅包含 SAAS 主库标准字段。 - 业务子类应继承 User 并在单独文件中添加业务字段和方法。 - 参见 gvsdsdk/business_models.py。 + 业务子类应继承 User 并添加业务字段与方法(如 gvsds.apps.core.models.User)。 """ import uuid as _uuid @@ -186,8 +187,7 @@ class User(QModel): """用户表——系统核心身份模型(抽象基类,映射 SAAS 主库 User 表标准字段) 本类为抽象基类,仅包含 SAAS 主库 User 表的标准字段和方法。 - 业务子类应继承本类,在单独文件中添加业务字段和方法。 - 参见 gvsdsdk/business_models.py。 + 业务子类应继承本类并添加业务字段与方法(如 gvsds.apps.core.models.User)。 """ UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID', default=_uuid.uuid4) @@ -202,6 +202,8 @@ class User(QModel): UserTele = models.CharField(max_length=16, null=True, blank=True, db_column='UserTele', verbose_name='电话') UserCreateTime = models.DateTimeField(default=_UtcNow, db_column='UserCreateTime', verbose_name='创建时间') UserLastLoginDate = models.DateTimeField(default=_UtcNow, db_column='UserLastLoginDate', verbose_name='最后登录') + IsStaff = models.BooleanField(default=False, db_column='IsStaff', verbose_name='是否员工') + IsSuperuser = models.BooleanField(default=False, db_column='IsSuperuser', verbose_name='是否超级用户') # Django auth 框架必需(由具体子类覆盖) USERNAME_FIELD = 'UserName' @@ -537,11 +539,13 @@ class UserRole(QModel): 通过 CompanyUUID 标识该用户-角色关联所属的公司, 用于菜单按公司差异化显示、数据权限按公司过滤等场景。 同一用户在不同公司下可绑定不同角色。 - DeptUUID、WorkgroupUUID 为可选字段,用于将关联进一步限定到部门或工作组。 RoleType 标识该用户在部门/工作组中的角色类型:0=普通成员/部门员工,1=组长/部门负责人。 RoleName 为可选的角色名覆盖:非空时前端展示用此名,为空时回退到 Role.RoleName。 这避免了为每个用户单独建角色,又能让同角色在不同用户视角下显示差异化名称。 + 注意:DeptUUID、WorkgroupUUID 为遗留字段(已废弃)——部门/工作组归属已迁移到 + UserRoleDepartment/UserRoleWorkgroup 关联表,字段仅保留兼容存量数据,禁止再次写入。 + 注意:UserUUID/RoleUUID/CompanyUUID/DeptUUID/WorkgroupUUID 在 gvsds 中是 ForeignKey, 但此处改为 UUIDField 以保持独立性(不支持 select_related 和关联属性访问)。 """ @@ -719,14 +723,24 @@ class PositionTemplate(QModel): ).values_list('PermUUID', flat=True) return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) - def CopyToRole(self, RoleName, TenantUUID): - """从模板复制权限到新角色(一次性复制,后续模板变更不影响已有角色)""" + def CopyToRole(self, RoleName, TenantUUID, CompanyUUID=None): + """从模板复制权限到新角色(一次性复制,后续模板变更不影响已有角色) + + Args: + RoleName: 新角色名称 + TenantUUID: 新角色所属租户 + CompanyUUID: 复制出的 RolePermission 所属公司。 + 缺省用系统公司 UUID(与主项目 RolePermission 创建一致), + RolePermission.CompanyUUID 为非空字段,漏传会导致插入失败。 + """ with transaction.atomic(): role = Role.objects.create( RoleUUID=_uuid.uuid4().bytes, TenantUUID=TenantUUID, RoleName=RoleName, ) + if CompanyUUID is None: + CompanyUUID = _uuid.UUID('00000000-0000-0000-0000-000000000001') template_perms = PositionTemplatePermission.objects.filter( PositionTemplateUUID=self.PositionTemplateUUID ) @@ -734,6 +748,7 @@ class PositionTemplate(QModel): RolePermission( RolePermUUID=_uuid.uuid4().bytes, RoleUUID=role.RoleUUID, + CompanyUUID=CompanyUUID, PermUUID=tp.PermUUID, ) for tp in template_perms @@ -793,4 +808,4 @@ class Position(QModel): def __str__(self): return self.PositionName -# (UserDept 表已废弃——员工部门关联改走 UserRole.DeptUUID,不再用独立表) +# (UserDept 表已废弃——员工部门归属已迁移到 UserRoleDepartment 关联表,不再用独立表) diff --git a/msync/__pycache__/client.cpython-312.pyc b/msync/__pycache__/client.cpython-312.pyc index 78db8b9..f0ffc98 100644 Binary files a/msync/__pycache__/client.cpython-312.pyc and b/msync/__pycache__/client.cpython-312.pyc differ diff --git a/msync/client.py b/msync/client.py index eb4f001..024f9e4 100644 --- a/msync/client.py +++ b/msync/client.py @@ -50,12 +50,6 @@ def _ResolveMasterURL(master_url): return DEFAULT_MASTER_URL -def _GenerateChallenge(): - """生成认证挑战字符串——与 auth.GenerateChallenge 统一格式(时间戳:UUID)""" - import time as _time - return f'{_time.time():.6f}:{uuid.uuid4().hex}' - - class ServiceClientError(Exception): def __init__(self, Message, StatusCode=None, ResponseBody=None): super().__init__(Message) diff --git a/payment/gateways/_test_write.txt b/payment/gateways/_test_write.txt deleted file mode 100644 index e69de29..0000000