加入了 GVSDSDK 模块,进行了 QModel 兼容层的尝试,生产环境可用

This commit is contained in:
2026-06-16 00:13:10 +08:00
parent 9d9cfa53a4
commit 63e0f4edfc
68 changed files with 10256 additions and 1143 deletions

50
gvsdsdk/auth/__init__.py Normal file
View File

@@ -0,0 +1,50 @@
"""gvsdsdk.auth — ELT 用户认证模块"""
__all__ = [
'ELTAuthentication',
'ELT_COOKIE_NAME', 'ELT_COOKIE_DOMAIN', 'ELT_COOKIE_MAX_AGE', 'SetELTCookie', 'DeleteELTCookie',
'HashPassword', 'VerifyPassword', 'SymmetricEncrypt', 'SymmetricDecrypt', 'FileSHA1', 'FileSHA256', 'BytesSHA1', 'BytesSHA256',
'ELTAuthMiddleware', 'CurrentRoleMiddleware',
'IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'PermissionVerifier',
'RoleManager', 'ShopManager', 'PositionManager',
]
def __getattr__(name):
if name == 'ELTAuthentication':
from gvsdsdk.auth.elt_auth import ELTAuthentication
globals()['ELTAuthentication'] = ELTAuthentication
return ELTAuthentication
if name in ('ELT_COOKIE_NAME', 'ELT_COOKIE_DOMAIN', 'ELT_COOKIE_MAX_AGE', 'SetELTCookie', 'DeleteELTCookie'):
from gvsdsdk.auth import cookie as _m
val = getattr(_m, name)
globals()[name] = val
return val
if name in ('HashPassword', 'VerifyPassword', 'SymmetricEncrypt', 'SymmetricDecrypt', 'FileSHA1', 'FileSHA256', 'BytesSHA1', 'BytesSHA256'):
from gvsdsdk.auth import crypto as _m
val = getattr(_m, name)
globals()[name] = val
return val
if name in ('ELTAuthMiddleware', 'CurrentRoleMiddleware'):
from gvsdsdk.auth import middleware as _m
val = getattr(_m, name)
globals()[name] = val
return val
if name in ('IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'PermissionVerifier'):
from gvsdsdk.auth import permissions as _m
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 == '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}")

33
gvsdsdk/auth/cookie.py Normal file
View File

@@ -0,0 +1,33 @@
from django.conf import settings
from django.http import HttpResponse
ELT_COOKIE_NAME = getattr(settings, 'ELT_COOKIE_NAME', 'ELToken')
ELT_COOKIE_DOMAIN = getattr(settings, 'ELT_COOKIE_DOMAIN', '.gvsds.com')
ELT_COOKIE_MAX_AGE = getattr(settings, 'ELT_COOKIE_MAX_AGE', 30 * 24 * 60 * 60)
def SetELTCookie(response, token_str):
response.set_cookie(
ELT_COOKIE_NAME, token_str,
domain=ELT_COOKIE_DOMAIN,
max_age=ELT_COOKIE_MAX_AGE,
secure=True, httponly=True, samesite='None',
)
def DeleteELTCookie(response):
response.delete_cookie(
ELT_COOKIE_NAME,
domain=ELT_COOKIE_DOMAIN,
samesite='None',
)
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')

93
gvsdsdk/auth/crypto.py Normal file
View File

@@ -0,0 +1,93 @@
import os
import bcrypt
import base64
import hashlib
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes as _crypto_hashes
def HashPassword(password, rounds=12):
if isinstance(password, str):
password = password.encode('utf-8')
return bcrypt.hashpw(password, bcrypt.gensalt(rounds=rounds))
def VerifyPassword(password, hashed):
if isinstance(password, str):
password = password.encode('utf-8')
if isinstance(hashed, str):
hashed = hashed.encode('utf-8')
return bcrypt.checkpw(password, hashed)
def _derive_key(master_secret):
"""使用 HKDF-SHA256 从任意长度的主密钥派生 256-bit AES 密钥"""
if isinstance(master_secret, str):
master_secret = master_secret.encode('utf-8')
hkdf = HKDF(
algorithm=_crypto_hashes.SHA256(),
length=32,
salt=b'gvsds-crypto-salt',
info=b'aes-gcm-256',
)
return hkdf.derive(master_secret)
def SymmetricEncrypt(plaintext, master_secret):
"""使用 AES-256-GCM 加密
输出格式: base64(nonce_12 + ciphertext + tag_16)
"""
if isinstance(plaintext, str):
plaintext = plaintext.encode('utf-8')
key = _derive_key(master_secret)
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ciphertext_with_tag = aesgcm.encrypt(nonce, plaintext, None)
return base64.b64encode(nonce + ciphertext_with_tag).decode('ascii')
def SymmetricDecrypt(ciphertext_b64, master_secret):
"""使用 AES-256-GCM 解密
输入格式: base64(nonce_12 + ciphertext + tag_16)
"""
raw = base64.b64decode(ciphertext_b64)
nonce = raw[:12]
ciphertext_with_tag = raw[12:]
key = _derive_key(master_secret)
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(nonce, ciphertext_with_tag, None)
return plaintext.decode('utf-8')
def FileSHA1(filepath):
sha1 = hashlib.sha1()
with open(filepath, 'rb') as f:
while True:
chunk = f.read(8192)
if not chunk:
break
sha1.update(chunk)
return sha1.hexdigest()
def FileSHA256(filepath):
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
while True:
chunk = f.read(8192)
if not chunk:
break
sha256.update(chunk)
return sha256.hexdigest()
def BytesSHA1(data):
return hashlib.sha1(data).hexdigest()
def BytesSHA256(data):
return hashlib.sha256(data).hexdigest()

46
gvsdsdk/auth/elt_auth.py Normal file
View File

@@ -0,0 +1,46 @@
import uuid
from gvsdsdk.auth.cookie import ELT_COOKIE_NAME
class ELTAuthentication:
def authenticate(self, request):
elt_token = request.COOKIES.get(ELT_COOKIE_NAME)
if not elt_token:
return None
try:
token_uuid = uuid.UUID(elt_token)
except (ValueError, AttributeError):
return None
from gvsdsdk.models import User, EphemeralToken
import datetime
try:
elt = EphemeralToken.objects.get(
ELTUUID=token_uuid,
ELTStatus=1,
)
except EphemeralToken.DoesNotExist:
return None
now_utc = datetime.datetime.now(datetime.timezone.utc)
if elt.ELTEndTime.replace(tzinfo=None) < now_utc.replace(tzinfo=None):
return None
# 滑动续期剩余时间不足7天时延长30天
remaining = elt.ELTEndTime.replace(tzinfo=None) - now_utc.replace(tzinfo=None)
if remaining < datetime.timedelta(days=7):
elt.ELTEndTime = now_utc + datetime.timedelta(days=30)
elt.save(update_fields=['ELTEndTime'])
try:
user = User.objects.get(UserUUID=elt.ELTUserUUID)
except User.DoesNotExist:
return None
request.elt = elt
return (user, elt_token)
def authenticate_header(self, request):
return 'Bearer'

265
gvsdsdk/auth/managers.py Normal file
View File

@@ -0,0 +1,265 @@
import uuid
import datetime
from django.contrib.auth.models import BaseUserManager
from gvsdsdk.models import Role, UserRole, RolePermission, Permission, Shop, RoleShop, Position, PositionTemplate, PositionTemplatePermission, UserDept
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=uuid.uuid4().bytes,
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 RoleManager:
"""角色管理器——提供角色创建、分配、权限管理"""
@staticmethod
def CreateRole(Name, TenantUUID, AssignScope=0, AssignScopeUUID=None, RoleType=2):
"""创建角色"""
role = Role(
RoleUUID=uuid.uuid4().bytes,
TenantUUID=TenantUUID,
RoleName=Name,
RoleType=RoleType,
RoleStatus=1,
AssignScope=AssignScope,
AssignScopeUUID=AssignScopeUUID,
RoleDesc='',
CreateTime=datetime.datetime.utcnow(),
)
role.save()
return role
@staticmethod
def AssignRole(UserUUID, RoleUUID, CompanyUUID):
"""为用户分配角色"""
user_role = UserRole(
UserRoleUUID=uuid.uuid4().bytes,
UserUUID=UserUUID,
RoleUUID=RoleUUID,
CompanyUUID=CompanyUUID,
CreateTime=datetime.datetime.utcnow(),
)
user_role.save()
return user_role
@staticmethod
def RemoveRole(UserUUID, RoleUUID, CompanyUUID=None):
"""移除用户的指定角色"""
qs = UserRole.objects.filter(UserUUID=UserUUID, RoleUUID=RoleUUID)
if CompanyUUID is not None:
qs = qs.filter(CompanyUUID=CompanyUUID)
deleted, _ = qs.delete()
return deleted > 0
@staticmethod
def GetRolesForUser(UserUUID, CompanyUUID=None):
"""获取用户的角色"""
qs = UserRole.objects.filter(UserUUID=UserUUID)
if CompanyUUID is not None:
qs = qs.filter(CompanyUUID=CompanyUUID)
role_uuids = qs.values_list('RoleUUID', flat=True)
return Role.objects.filter(RoleUUID__in=role_uuids)
@staticmethod
def GetUsersForRole(RoleUUID):
"""获取拥有指定角色的所有用户"""
from gvsdsdk.models import User
user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True)
return User.objects.filter(UserUUID__in=user_uuids)
@staticmethod
def AssignPermission(RoleUUID, PermUUID):
"""为角色分配权限"""
rp = RolePermission(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=RoleUUID,
PermUUID=PermUUID,
CreateTime=datetime.datetime.utcnow(),
)
rp.save()
return rp
@staticmethod
def RemovePermission(RoleUUID, PermUUID):
"""移除角色的指定权限"""
deleted, _ = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID).delete()
return deleted > 0
@staticmethod
def GetPermissionsForRole(RoleUUID):
"""获取角色的所有权限"""
perm_uuids = RolePermission.objects.filter(RoleUUID=RoleUUID).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=datetime.datetime.utcnow(),
UpdateTime=datetime.datetime.utcnow(),
)
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=datetime.datetime.utcnow(),
)
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=datetime.datetime.utcnow(),
UpdateTime=datetime.datetime.utcnow(),
)
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=datetime.datetime.utcnow(),
UpdateTime=datetime.datetime.utcnow(),
)
template.save()
return template
@staticmethod
def AssignPermToTemplate(TemplateUUID, PermUUID):
"""为岗位模板关联权限"""
ptp = PositionTemplatePermission(
PTPermUUID=uuid.uuid4().bytes,
PositionTemplateUUID=TemplateUUID,
PermUUID=PermUUID,
CreateTime=datetime.datetime.utcnow(),
)
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)

View File

@@ -0,0 +1,54 @@
from gvsdsdk.auth.cookie import ELT_COOKIE_NAME
from gvsdsdk.models import UserRole, Role
from gvsdsdk.model_utils import UUIDObj
class ELTAuthMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
elt_token = request.COOKIES.get(ELT_COOKIE_NAME)
if elt_token:
request.elt_token = elt_token
response = self.get_response(request)
return response
class CurrentRoleMiddleware:
"""解析 CurrentRoleUUID Cookie设置 request.current_role 和 request.current_company_uuid
request.current_role : gvsdsdk.models.Role 实例(或 None
request.current_company_uuid : 通过 UserRole.CompanyUUID 获取当前角色所属公司UUID
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.current_role = None
request.current_company_uuid = None
# 仅对已认证用户处理角色
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
role_uuid_str = request.COOKIES.get('CurrentRoleUUID', '')
if role_uuid_str:
try:
role_uuid_bytes = UUIDObj(role_uuid_str).bytes
# gvsdsdk.UserRole 的 RoleUUID 是 UUIDField不能用 select_related
user_role = UserRole.objects.filter(
UserUUID=request.user.UserUUID,
RoleUUID=role_uuid_bytes
).first()
if user_role:
# 单独查询 Role 实例
role = Role.objects.filter(RoleUUID=role_uuid_bytes).first()
if role:
request.current_role = role
# 通过 UserRole.CompanyUUID 获取当前公司
if user_role.CompanyUUID:
request.current_company_uuid = user_role.CompanyUUID
except Exception:
pass
return self.get_response(request)

109
gvsdsdk/auth/permissions.py Normal file
View File

@@ -0,0 +1,109 @@
from gvsdsdk.models import User, UserRole, Role, RolePermission, Permission, UserABAC
from rest_framework.permissions import BasePermission
class IsAdminUser(BasePermission):
"""仅允许管理员IsStaff 或 IsSuperuser访问"""
def has_permission(self, request, view):
user = request.user
if not user or not user.is_authenticated:
return False
if not isinstance(user, User):
return False
return user.IsStaff or user.IsSuperuser
class IsCompanyMember(BasePermission):
"""验证用户当前角色属于目标数据所在公司"""
def has_object_permission(self, request, view, obj):
if not request.current_company_uuid:
return False
obj_company = getattr(obj, 'CompanyUUID_id', None) or getattr(obj, 'CompanyUUID', None)
if obj_company is None:
return True # 无公司归属的数据不做限制
return str(obj_company) == str(request.current_company_uuid)
class CompanyScopedMixin:
"""ViewSet Mixin自动按当前角色所属公司过滤 queryset"""
company_field = 'CompanyUUID' # 可在 ViewSet 中覆盖
def get_queryset(self):
qs = super().get_queryset()
company_uuid = getattr(self.request, 'current_company_uuid', None)
if company_uuid and hasattr(qs.model, self.company_field):
qs = qs.filter(**{self.company_field: company_uuid})
return qs
def perform_create(self, serializer):
company_uuid = getattr(self.request, 'current_company_uuid', None)
if company_uuid and hasattr(serializer.Meta.model, 'CompanyUUID'):
serializer.save(CompanyUUID=company_uuid)
else:
serializer.save()
def PermissionVerifier(permission, attributes, user=None, request=None):
"""权限验证器——混合 ABAC + RBAC 检查
RBAC 仅检查当前角色CurrentRoleUUID Cookie 指定的角色)的权限,
通过 RolePermission → Permission 链路查询。
超级管理员/员工直接放行。
"""
if not permission:
return True
if user is None:
return False
if not isinstance(user, User):
return False
# 超级管理员和员工直接放行
if user.IsSuperuser or user.IsStaff:
return True
current_role = getattr(request, 'current_role', None) if request else None
for attr in attributes:
# ABAC 检查
abac_match = UserABAC.objects.filter(
UserUUID=user.UserUUID,
HierarchicalPermissionIdentifiers=attr
).first()
if abac_match:
continue
# RBAC 检查:仅检查当前角色
if current_role and isinstance(current_role, Role):
if current_role.RoleStatus != 1:
return False
perm_uuids = RolePermission.objects.filter(
RoleUUID=current_role.RoleUUID
).values_list('PermUUID', flat=True)
rbac_result = Permission.objects.filter(
PermUUID__in=perm_uuids,
PermCode=attr,
PermStatus=1,
).exists()
else:
# 无 current_role 时回退到用户所有活跃角色
user_role_uuids = UserRole.objects.filter(
UserUUID=user.UserUUID
).values_list('RoleUUID', flat=True)
active_role_uuids = Role.objects.filter(
RoleUUID__in=user_role_uuids,
RoleStatus=1,
).values_list('RoleUUID', flat=True)
perm_uuids = RolePermission.objects.filter(
RoleUUID__in=active_role_uuids
).values_list('PermUUID', flat=True)
rbac_result = Permission.objects.filter(
PermUUID__in=perm_uuids,
PermCode=attr,
PermStatus=1,
).exists()
if rbac_result:
continue
return False
return True