"feat(sdk): 新增审计/默认角色/作用域与用户级联能力
RoleManager 补齐 EnsureDefaultRoleForUser/EnsureTenantAdminRole/ GetUserRoleRelation/GetScopeMembers,新增 AdminUserManager.DeleteUser、 AuditManager、StatsManager 列表方法;新增 AuditLog/UserRoleWorkgroup 模型。"
This commit is contained in:
1 parent
dc4aa44562
commit
277b2f3043
4 files changed
+342
-2
No files matched your search
+10
@@ -301,6 +301,16 @@ _LAZY_SYMBOLS = {
|
||||
'PositionTemplatePermission': ('models', 'PositionTemplatePermission'),
|
||||
'Menu': ('models', 'Menu'),
|
||||
'WorkOrder': ('models', 'WorkOrder'),
|
||||
'AuditLog': ('models', 'AuditLog'),
|
||||
'UserRoleDepartment': ('models', 'UserRoleDepartment'),
|
||||
'UserRoleWorkgroup': ('models', 'UserRoleWorkgroup'),
|
||||
'Message': ('models', 'Message'),
|
||||
'UserIDCard': ('models', 'UserIDCard'),
|
||||
'PersonalPage': ('models', 'PersonalPage'),
|
||||
'UserSettings': ('models', 'UserSettings'),
|
||||
'Tenant': ('models', 'Tenant'),
|
||||
'Company': ('models', 'Company'),
|
||||
'Department': ('models', 'Department'),
|
||||
'FixedBinaryField': ('models', 'FixedBinaryField'),
|
||||
'QModel': ('model_base', 'QModel'),
|
||||
'QModelBase': ('model_base', 'QModelBase'),
|
||||
|
||||
+258
-1
@@ -2,10 +2,13 @@ import uuid
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone as _timezone
|
||||
|
||||
from gvsdsdk.models import Role, UserRole, RolePermission, Permission, Position, PositionTemplate, PositionTemplatePermission, Shop, RoleShop, User2FA, UserAvatar, Password, UserPending2FA, AccountActivation, Tenant, Company, Department, UserRoleDepartment, Message, Menu, WorkOrder
|
||||
from gvsdsdk.models import Role, UserRole, UserRoleDepartment, UserRoleWorkgroup, RolePermission, Permission, Position, PositionTemplate, PositionTemplatePermission, Shop, RoleShop, User2FA, UserAvatar, Password, UserPending2FA, AccountActivation, Tenant, Company, Department, Message, Menu, WorkOrder, AuditLog
|
||||
from gvsdsdk.auth.crypto import HashPassword, VerifyPassword
|
||||
from gvsdsdk.model_utils import _GetUserModel, UUIDObj
|
||||
|
||||
# 系统公司 UUID(00000000-0000-0000-0000-000000000001),全局唯一
|
||||
_SYSTEM_COMPANY_UUID = uuid.UUID('00000000-0000-0000-0000-000000000001').bytes
|
||||
|
||||
# NOTE: 类型注解暂缓——RoleManager/PositionManager 静态方法较多,全量补充注解工作量较大且易引入回归,后续单独迭代处理。
|
||||
|
||||
|
||||
@@ -176,6 +179,68 @@ class RoleManager:
|
||||
qs = qs.filter(CompanyUUID=CompanyUUID)
|
||||
return list(qs)
|
||||
|
||||
@staticmethod
|
||||
def GetUserRoleRelation(UserUUID, RoleUUID, CompanyUUID=None):
|
||||
"""按 (用户, 角色[, 公司]) 查询 UserRole 关联(不存在返回 None)"""
|
||||
qs = UserRole.objects.filter(
|
||||
UserUUID=UUIDObj(UserUUID).bytes,
|
||||
RoleUUID=UUIDObj(RoleUUID).bytes,
|
||||
)
|
||||
if CompanyUUID is not None:
|
||||
try:
|
||||
qs = qs.filter(CompanyUUID=UUIDObj(CompanyUUID).bytes)
|
||||
except Exception:
|
||||
qs = qs.filter(CompanyUUID=CompanyUUID)
|
||||
return qs.first()
|
||||
|
||||
@staticmethod
|
||||
def GetUserRoleDepartment(UserRoleUUID):
|
||||
"""按 UserRoleUUID 查询部门归属(不存在返回 None)"""
|
||||
return UserRoleDepartment.objects.filter(
|
||||
UserRoleUUID=UUIDObj(UserRoleUUID).bytes,
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def GetUserRoleWorkgroup(UserRoleUUID):
|
||||
"""按 UserRoleUUID 查询工作组归属(不存在返回 None)"""
|
||||
return UserRoleWorkgroup.objects.filter(
|
||||
UserRoleUUID=UUIDObj(UserRoleUUID).bytes,
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def GetScopeMembers(ScopeType, UUID):
|
||||
"""获取作用域成员的用户 UUID 列表(去重)
|
||||
|
||||
Args:
|
||||
ScopeType: 'dept_manager' | 'workgroup_lead'
|
||||
UUID: 部门或工作组 UUID(bytes/str/UUIDObj 均可)
|
||||
|
||||
Returns:
|
||||
list[bytes]:成员用户 UUID;无成员返回 [];未知类型或异常返回 None
|
||||
"""
|
||||
try:
|
||||
uuid_bytes = UUIDObj(UUID).bytes
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
if ScopeType == 'dept_manager':
|
||||
ur_uuids = list(UserRoleDepartment.objects.filter(
|
||||
DeptUUID=uuid_bytes,
|
||||
).values_list('UserRoleUUID', flat=True))
|
||||
elif ScopeType == 'workgroup_lead':
|
||||
ur_uuids = list(UserRoleWorkgroup.objects.filter(
|
||||
WorkgroupUUID=uuid_bytes,
|
||||
).values_list('UserRoleUUID', flat=True))
|
||||
else:
|
||||
return None
|
||||
if not ur_uuids:
|
||||
return []
|
||||
# UserRoleUUID → UserUUID(关联表存 UserRoleUUID,用户 UUID 从 UserRole 表取)
|
||||
qs = UserRole.objects.filter(UserRoleUUID__in=ur_uuids)
|
||||
return list(set(qs.values_list('UserUUID', flat=True)))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def HasRolePermission(RoleUUID, PermUUID, CompanyUUID=None):
|
||||
"""检查角色是否已拥有指定权限(同公司下防重复绑定)"""
|
||||
@@ -456,6 +521,103 @@ class RoleManager:
|
||||
return None, False
|
||||
return False, True
|
||||
|
||||
@staticmethod
|
||||
def EnsureDefaultRoleForUser(UserUUID, CompanyUUID, AdminUserUUID=None):
|
||||
"""为新用户关联全局默认角色("默认角色"在系统租户下唯一)
|
||||
|
||||
租户解析顺序:系统公司所属租户 → 操作管理员角色租户 → 系统首个租户。
|
||||
|
||||
Args:
|
||||
UserUUID: 新用户 UUID(bytes/str/UUIDObj 均可)
|
||||
CompanyUUID: 公司 UUID(通常为系统公司)
|
||||
AdminUserUUID: 可选,操作管理员 UUID(租户回退用)
|
||||
|
||||
Returns:
|
||||
(UserRole 实例 or None, 错误消息 or None)
|
||||
"""
|
||||
company_bytes = UUIDObj(CompanyUUID).bytes
|
||||
# 角色租户优先取自系统公司所属租户
|
||||
tenant_uuid = None
|
||||
company = Company.objects.filter(CompanyUUID=company_bytes).first()
|
||||
if company is not None:
|
||||
tenant_uuid = company.TenantUUID
|
||||
# 回退:当前操作管理员的首个角色租户
|
||||
if not tenant_uuid and AdminUserUUID:
|
||||
try:
|
||||
admin_ur = UserRole.objects.filter(
|
||||
UserUUID=UUIDObj(AdminUserUUID).bytes,
|
||||
).first()
|
||||
if admin_ur and admin_ur.RoleUUID:
|
||||
role = Role.objects.filter(RoleUUID=admin_ur.RoleUUID).first()
|
||||
tenant_uuid = role.TenantUUID if role else None
|
||||
except Exception:
|
||||
tenant_uuid = None
|
||||
# 回退:系统首个租户
|
||||
if not tenant_uuid:
|
||||
tenant = Tenant.objects.first()
|
||||
tenant_uuid = tenant.TenantUUID if tenant else None
|
||||
if not tenant_uuid:
|
||||
return None, '无法确定租户'
|
||||
|
||||
# 查找或创建全局唯一的"默认角色"
|
||||
default_role = Role.objects.filter(
|
||||
TenantUUID=tenant_uuid,
|
||||
RoleName='默认角色',
|
||||
).first()
|
||||
if not default_role:
|
||||
default_role = Role.objects.create(
|
||||
RoleUUID=uuid.uuid4().bytes,
|
||||
TenantUUID=tenant_uuid,
|
||||
RoleName='默认角色',
|
||||
RoleStatus=1,
|
||||
AssignScope=0,
|
||||
RoleDesc='系统默认角色,所有新用户自动关联',
|
||||
CreateTime=_timezone.now(),
|
||||
)
|
||||
|
||||
# 避免重复关联
|
||||
if RoleManager.HasRole(UserUUID, default_role.RoleUUID, CompanyUUID):
|
||||
return None, None
|
||||
ur = UserRole.objects.create(
|
||||
UserRoleUUID=uuid.uuid4().bytes,
|
||||
UserUUID=UUIDObj(UserUUID).bytes,
|
||||
RoleUUID=default_role.RoleUUID,
|
||||
CompanyUUID=company_bytes,
|
||||
)
|
||||
return ur, None
|
||||
|
||||
@staticmethod
|
||||
def EnsureTenantAdminRole(TenantUUID):
|
||||
"""为租户(集团)创建/复用"集团超级管理员"角色并分配 admin:tenant 权限
|
||||
|
||||
Args:
|
||||
TenantUUID: 租户 UUID(bytes/str/UUIDObj 均可)
|
||||
|
||||
Returns:
|
||||
Role 实例(已创建或已存在)
|
||||
"""
|
||||
tenant_bytes = UUIDObj(TenantUUID).bytes
|
||||
role = Role.objects.filter(
|
||||
TenantUUID=tenant_bytes,
|
||||
RoleName='集团超级管理员',
|
||||
).first()
|
||||
if role:
|
||||
return role
|
||||
|
||||
role = RoleManager.CreateRole(
|
||||
'集团超级管理员', tenant_bytes,
|
||||
AssignScope=0,
|
||||
RoleDesc='集团超级管理员,管理本租户下所有事务',
|
||||
)
|
||||
|
||||
# 分配 admin:tenant 权限(租户内管理员:关闭公司隔离,保留租户隔离)
|
||||
admin_perm = RoleManager.GetPermissionByCode('admin:tenant')
|
||||
if admin_perm:
|
||||
RoleManager.AssignPermission(
|
||||
role.RoleUUID, admin_perm.PermUUID, _SYSTEM_COMPANY_UUID,
|
||||
)
|
||||
return role
|
||||
|
||||
|
||||
class PositionManager:
|
||||
"""职务管理器——提供人事职务标签创建、岗位模板管理"""
|
||||
@@ -1022,6 +1184,56 @@ class AuthManager:
|
||||
class AdminUserManager:
|
||||
"""管理端用户管理器——用户搜索、密码重置、账户状态与邮箱管理"""
|
||||
|
||||
@staticmethod
|
||||
def DeleteUser(UserUUID):
|
||||
"""删除用户并级联清理关联数据(UserRole / 专属默认角色 / UserAvatar / User2FA / UserPending2FA)
|
||||
|
||||
与 gvsds FormService.delete_record 用户分支零行为差异:
|
||||
仅当角色名为 "{用户名}的默认角色" 且无其他用户持有时才删除角色本身。
|
||||
|
||||
Args:
|
||||
UserUUID: 用户 UUID(bytes/str/UUIDObj 均可)
|
||||
|
||||
Returns:
|
||||
(成功?, 错误消息 or None)
|
||||
"""
|
||||
User = _GetUserModel()
|
||||
try:
|
||||
user_bytes = UUIDObj(UserUUID).bytes
|
||||
except Exception:
|
||||
return False, '无效的 UserUUID'
|
||||
user = User.objects.filter(UserUUID=user_bytes).first()
|
||||
if user is None:
|
||||
return False, '用户不存在'
|
||||
|
||||
from django.db import transaction
|
||||
with transaction.atomic():
|
||||
user_name = getattr(user, 'UserName', '') or ''
|
||||
# 查找该用户的所有 UserRole 关联
|
||||
user_roles = list(UserRole.objects.filter(UserUUID=user_bytes))
|
||||
# 识别默认角色:名称匹配 "{用户名}的默认角色" 且仅此用户一人
|
||||
default_role_ids = []
|
||||
for ur in user_roles:
|
||||
role = Role.objects.filter(RoleUUID=ur.RoleUUID).first()
|
||||
if role and getattr(role, 'RoleName', '') == f'{user_name}的默认角色':
|
||||
other_count = UserRole.objects.filter(
|
||||
RoleUUID=role.RoleUUID,
|
||||
).exclude(UserUUID=user_bytes).count()
|
||||
if other_count == 0:
|
||||
default_role_ids.append(role.RoleUUID)
|
||||
# 删除所有 UserRole 关联
|
||||
UserRole.objects.filter(UserUUID=user_bytes).delete()
|
||||
# 删除默认角色(RolePermission 会被 cascade 删除)
|
||||
if default_role_ids:
|
||||
Role.objects.filter(RoleUUID__in=default_role_ids).delete()
|
||||
# 清理 UserAvatar / User2FA / UserPending2FA(这些表用 UUIDField 而非 ForeignKey,不会自动级联)
|
||||
UserAvatar.objects.filter(UserUUID=user_bytes).delete()
|
||||
User2FA.objects.filter(UserUUID=user_bytes).delete()
|
||||
UserPending2FA.objects.filter(UserUUID=user_bytes).delete()
|
||||
# 删除用户本身
|
||||
user.delete()
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def SearchUsers(Keyword='', Offset=0, PageSize=20):
|
||||
"""搜索用户(按用户名模糊匹配,分页)
|
||||
@@ -1771,3 +1983,48 @@ class WorkOrderManager:
|
||||
wo.save()
|
||||
return WorkOrderManager._ToFlat(wo)
|
||||
|
||||
|
||||
class AuditManager:
|
||||
"""审计日志管理器——敏感表写操作快照记录(失败静默,不阻断业务)"""
|
||||
|
||||
@staticmethod
|
||||
def Log(TenantUUID=None, CompanyUUID=None, UserUUID=None, TableName='',
|
||||
Action='', TargetUUID=None, IPAddress='', OldData=None, NewData=None):
|
||||
"""写入一条审计日志
|
||||
|
||||
Args:
|
||||
TenantUUID/CompanyUUID/UserUUID: 归属与操作人 UUID(bytes/str/UUIDObj 均可,无效自动置空)
|
||||
TableName: 表名
|
||||
Action: 操作类型(add/modify/delete/xxx_status)
|
||||
TargetUUID: 目标记录 UUID
|
||||
IPAddress: 操作 IP
|
||||
OldData/NewData: 变更前后数据快照
|
||||
|
||||
Returns:
|
||||
True
|
||||
"""
|
||||
def _ToBytes(value):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return UUIDObj(value).bytes
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
AuditLog.objects.create(
|
||||
AuditLogUUID=uuid.uuid4().bytes,
|
||||
TenantUUID=_ToBytes(TenantUUID),
|
||||
CompanyUUID=_ToBytes(CompanyUUID),
|
||||
UserUUID=_ToBytes(UserUUID),
|
||||
TableName=TableName or '',
|
||||
Action=Action or '',
|
||||
TargetUUID=_ToBytes(TargetUUID),
|
||||
IPAddress=IPAddress or '',
|
||||
OldData=OldData,
|
||||
NewData=NewData,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
@@ -537,6 +537,26 @@ class UserRoleDepartment(QModel):
|
||||
verbose_name_plural = '用户-角色部门关联'
|
||||
|
||||
|
||||
# === 用户-角色工作组关联表 ===
|
||||
|
||||
class UserRoleWorkgroup(QModel):
|
||||
"""用户-角色对 与 工作组 的多对多关联表(映射 SAAS 主库 UserRoleWorkgroup 表)"""
|
||||
|
||||
UserRoleWorkgroupUUID = UUIDField(primary_key=True, db_column='UserRoleWorkgroupUUID', verbose_name='关联UUID')
|
||||
UserRoleUUID = UUIDField(db_column='UserRoleUUID', verbose_name='用户-角色关联UUID')
|
||||
WorkgroupUUID = UUIDField(db_column='WorkgroupUUID', verbose_name='工作组UUID')
|
||||
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
RoleType = models.SmallIntegerField(default=0, db_column='RoleType', verbose_name='角色类型')
|
||||
CreateTime = models.DateTimeField(default=_UtcNow, db_column='CreateTime', verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'UserRoleWorkgroup'
|
||||
verbose_name = '用户-角色工作组关联'
|
||||
verbose_name_plural = '用户-角色工作组关联'
|
||||
|
||||
|
||||
# === 消息表 ===
|
||||
|
||||
class Message(QModel):
|
||||
@@ -1050,3 +1070,32 @@ class WorkOrder(QModel):
|
||||
|
||||
def __str__(self):
|
||||
return self.Title
|
||||
|
||||
|
||||
# === 操作审计日志表 ===
|
||||
|
||||
class AuditLog(QModel):
|
||||
"""操作审计日志表——记录敏感表写操作快照(映射 SAAS 主库 AuditLog 表)"""
|
||||
|
||||
AuditLogUUID = UUIDField(primary_key=True, db_column='AuditLogUUID', verbose_name='日志UUID')
|
||||
TenantUUID = UUIDField(null=True, blank=True, db_column='TenantUUID', verbose_name='租户UUID')
|
||||
CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
UserUUID = UUIDField(null=True, blank=True, db_column='UserUUID', verbose_name='操作人UUID')
|
||||
TableName = models.CharField(max_length=64, db_index=True, db_column='TableName', verbose_name='表名')
|
||||
Action = models.CharField(max_length=16, db_column='Action', verbose_name='操作类型')
|
||||
TargetUUID = UUIDField(null=True, blank=True, db_column='TargetUUID', verbose_name='目标记录UUID')
|
||||
IPAddress = models.CharField(max_length=64, default='', db_column='IPAddress', verbose_name='IP地址')
|
||||
OldData = models.JSONField(null=True, blank=True, db_column='OldData', verbose_name='变更前数据')
|
||||
NewData = models.JSONField(null=True, blank=True, db_column='NewData', verbose_name='变更后数据')
|
||||
CreateTime = models.DateTimeField(default=_UtcNow, db_index=True, db_column='CreateTime', verbose_name='创建时间')
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
app_label = 'gvsdsdk'
|
||||
db_table = 'AuditLog'
|
||||
ordering = ['-CreateTime']
|
||||
verbose_name = '操作审计日志'
|
||||
verbose_name_plural = '操作审计日志'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.TableName}/{self.Action}'
|
||||
+25
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import F, Sum, Count
|
||||
from gvsdsdk.stats.models import StatMetric, StatRecord
|
||||
from gvsdsdk.stats.models import StatMetric, StatRecord, StatDimension, StatConfig
|
||||
|
||||
|
||||
class StatsManager:
|
||||
@@ -313,3 +313,27 @@ class StatsManager:
|
||||
)
|
||||
count, _ = qs.delete()
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def ListMetrics(tenant_uuid, status=None):
|
||||
"""列出租户下的统计指标(可按状态过滤,保持数据库顺序)"""
|
||||
qs = StatMetric.objects.filter(TenantUUID=tenant_uuid)
|
||||
if status is not None:
|
||||
qs = qs.filter(MetricStatus=status)
|
||||
return qs
|
||||
|
||||
@staticmethod
|
||||
def ListDimensions(tenant_uuid, status=None):
|
||||
"""列出租户下的统计维度(可按状态过滤,保持数据库顺序)"""
|
||||
qs = StatDimension.objects.filter(TenantUUID=tenant_uuid)
|
||||
if status is not None:
|
||||
qs = qs.filter(DimensionStatus=status)
|
||||
return qs
|
||||
|
||||
@staticmethod
|
||||
def ListConfigs(tenant_uuid, status=None):
|
||||
"""列出租户下的统计配置(可按状态过滤,保持数据库顺序)"""
|
||||
qs = StatConfig.objects.filter(TenantUUID=tenant_uuid)
|
||||
if status is not None:
|
||||
qs = qs.filter(ConfigStatus=status)
|
||||
return qs
|
||||
Reference in new issue
Block a user