1045 lines
42 KiB
Python
1045 lines
42 KiB
Python
"""GVS-DSDK 数据模型模块
|
||
|
||
将 SAAS 主服务器的用户表(User)、权限表(Permission)、角色表(Role)、
|
||
用户角色关联表(UserRole)、角色权限关联表(RolePermission)、
|
||
岗位模板表(PositionTemplate)、岗位模板权限关联表(PositionTemplatePermission)、
|
||
人事职务标签表(Position)、员工部门关联表(UserDept)封装为独立的 Django ORM 模型,
|
||
供子服务器直接进行跨服务器 ORM 查询。
|
||
|
||
权限体系四大分层:
|
||
1. 组织人事层:Tenant → Company → Department,员工通过 UserDept 归属部门
|
||
2. 岗位模板层:PositionTemplate + PositionTemplatePermission,权限初始化快照
|
||
3. 角色权限载体层:Role + RolePermission,运行时鉴权实体
|
||
4. 鉴权规则层:操作权限校验(角色汇总)+ 数据权限校验(组织属性)
|
||
|
||
使用方式(子服务器):
|
||
from gvsdsdk.models import User, Permission, Role, UserRole, RolePermission
|
||
|
||
# 查询所有启用的用户
|
||
users = User.objects.using('saas').filter(UserAccountLicense=1)
|
||
|
||
# 或配置 DatabaseRouter 后直接使用
|
||
users = User.objects.filter(UserAccountLicense=1)
|
||
|
||
设计原则:
|
||
1. 独立:不依赖 gvsds 内部模块,可在任意 Django 项目中独立使用
|
||
2. 非托管:managed=False,不创建/迁移表,仅映射已有表结构
|
||
3. PascalCase:字段名、方法名遵循帕斯卡命名法
|
||
4. 兼容:db_table、db_column 与 SAAS 主库完全一致
|
||
5. 租户隔离:所有权限相关数据依附于独立租户空间,不同租户互相不可见
|
||
"""
|
||
|
||
import uuid as _uuid
|
||
import datetime as _datetime
|
||
from django.db import models
|
||
from django.contrib.auth.models import BaseUserManager
|
||
from gvsdsdk.model_base import QModel
|
||
from gvsdsdk.model_utils import UUIDObj
|
||
|
||
|
||
# === UUID 字段 ===
|
||
|
||
class FixedBinaryField(models.BinaryField):
|
||
"""定长二进制字段:映射 MySQL BINARY(N),可用于主键"""
|
||
|
||
description = "Fixed-length binary field"
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
self.binary_length = kwargs.pop('binary_length', 16)
|
||
kwargs.setdefault('max_length', self.binary_length)
|
||
kwargs.setdefault('editable', False)
|
||
super().__init__(*args, **kwargs)
|
||
|
||
def db_type(self, connection):
|
||
if connection.vendor == 'mysql':
|
||
return f'BINARY({self.binary_length})'
|
||
elif connection.vendor == 'sqlite':
|
||
return 'BLOB'
|
||
elif connection.vendor == 'postgresql':
|
||
return 'BYTEA'
|
||
return f'BINARY({self.binary_length})'
|
||
|
||
def rel_db_type(self, connection):
|
||
return self.db_type(connection)
|
||
|
||
def deconstruct(self):
|
||
name, path, args, kwargs = super().deconstruct()
|
||
kwargs['binary_length'] = self.binary_length
|
||
return name, path, args, kwargs
|
||
|
||
|
||
class UUIDField(FixedBinaryField):
|
||
"""UUID 字段:数据库存储为 BINARY(16),Python 侧自动转换 str/bytes/uuid.UUID"""
|
||
|
||
description = "UUID stored as BINARY(16)"
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
kwargs.setdefault('max_length', 16)
|
||
kwargs.setdefault('editable', False)
|
||
super().__init__(*args, **kwargs)
|
||
|
||
def db_type(self, connection):
|
||
vendor = connection.vendor
|
||
if vendor == 'mysql':
|
||
return 'BINARY(16)'
|
||
elif vendor == 'sqlite':
|
||
return 'BLOB'
|
||
elif vendor == 'postgresql':
|
||
return 'BYTEA'
|
||
return 'BINARY(16)'
|
||
|
||
def from_db_value(self, value, expression, connection):
|
||
if value is None:
|
||
return None
|
||
if isinstance(value, UUIDObj):
|
||
return value
|
||
return UUIDObj(value)
|
||
|
||
def get_prep_value(self, value):
|
||
if value is None:
|
||
return None
|
||
if isinstance(value, UUIDObj):
|
||
return value.bytes
|
||
if isinstance(value, str):
|
||
try:
|
||
return _uuid.UUID(value).bytes
|
||
except ValueError:
|
||
return None
|
||
if isinstance(value, _uuid.UUID):
|
||
return value.bytes
|
||
if isinstance(value, bytes):
|
||
return value
|
||
return None
|
||
|
||
def get_prep_lookup(self, lookup_type, value):
|
||
if lookup_type in ('exact', 'iexact', 'in'):
|
||
if value is None:
|
||
return None
|
||
if lookup_type == 'in' and isinstance(value, (list, tuple)):
|
||
return [self.get_prep_value(v) for v in value]
|
||
return self.get_prep_value(value)
|
||
return super().get_prep_lookup(lookup_type, value)
|
||
|
||
def value_to_string(self, obj):
|
||
value = self.value_from_object(obj)
|
||
if isinstance(value, UUIDObj):
|
||
return value.str or ''
|
||
if isinstance(value, str):
|
||
return value
|
||
if isinstance(value, bytes) and len(value) == 16:
|
||
return str(_uuid.UUID(bytes=value))
|
||
if isinstance(value, _uuid.UUID):
|
||
return str(value)
|
||
return ''
|
||
|
||
def deconstruct(self):
|
||
name, path, args, kwargs = super().deconstruct()
|
||
del kwargs['max_length']
|
||
return name, path, args, kwargs
|
||
|
||
|
||
# === QModel 基础模型 ===
|
||
|
||
# === 用户管理器 ===
|
||
|
||
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 User(QModel):
|
||
"""用户表——系统核心身份模型(映射 SAAS 主库 User 表)"""
|
||
|
||
UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID', default=_uuid.uuid4)
|
||
UserName = models.CharField(max_length=256, unique=True, db_column='UserName', verbose_name='用户名')
|
||
UserPassword = models.CharField(max_length=60, default='', db_column='UserPassword', verbose_name='密码')
|
||
UserPositionStatus = models.SmallIntegerField(default=0, db_column='UserPositionStatus', verbose_name='职位状态')
|
||
UserPersonalSignature = models.CharField(max_length=1024, default='', db_column='UserPersonalSignature', verbose_name='个人签名')
|
||
UserEmail = models.CharField(max_length=256, default='', db_column='UserEmail', verbose_name='邮箱')
|
||
UserEmailVerify = models.SmallIntegerField(default=0, db_column='UserEmailVerify', verbose_name='邮箱验证')
|
||
UserAccountLicense = models.SmallIntegerField(default=1, db_column='UserAccountLicense', verbose_name='账户许可')
|
||
UserGender = models.SmallIntegerField(default=-1, db_column='UserGender', verbose_name='性别')
|
||
UserTele = models.CharField(max_length=16, null=True, blank=True, db_column='UserTele', verbose_name='电话')
|
||
UserCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserCreateTime', verbose_name='创建时间')
|
||
UserLastLoginDate = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserLastLoginDate', verbose_name='最后登录')
|
||
# === 本地业务字段(去远程化后新增) ===
|
||
UserUID = models.CharField(max_length=7, unique=True, db_column='UserUID', verbose_name='用户业务ID', null=True, blank=True)
|
||
OpenID = models.CharField(max_length=64, unique=True, db_column='OpenID', verbose_name='微信OpenID', null=True, blank=True)
|
||
UnionID = models.CharField(max_length=64, db_column='UnionID', verbose_name='微信UnionID', null=True, blank=True)
|
||
Avatar = models.CharField(max_length=500, default='a_long/morentouxiang.jpg', db_column='Avatar', verbose_name='头像')
|
||
Phone = models.CharField(max_length=11, db_column='Phone', verbose_name='手机号', null=True, blank=True)
|
||
PhoneVerified = models.BooleanField(default=False, db_column='PhoneVerified', verbose_name='手机号是否已认证')
|
||
IP = models.CharField(max_length=50, db_column='IP', verbose_name='来访用户IP', null=True, blank=True)
|
||
IsStaff = models.BooleanField(default=False, db_column='IsStaff', verbose_name='是否员工')
|
||
IsSuperuser = models.BooleanField(default=False, db_column='IsSuperuser', verbose_name='是否超级用户')
|
||
|
||
objects = UserManager()
|
||
|
||
# Django auth 框架必需
|
||
USERNAME_FIELD = 'UserName'
|
||
REQUIRED_FIELDS = ['UserUID']
|
||
|
||
class Meta:
|
||
managed = True # 将由 QModelBase.__new__ 根据 GVSDSDK_MANAGE_TABLES 动态设置
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'User'
|
||
verbose_name = '用户'
|
||
verbose_name_plural = '用户'
|
||
indexes = [
|
||
models.Index(fields=['UserUID']),
|
||
models.Index(fields=['OpenID']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return self.UserName
|
||
|
||
@property
|
||
def UUIDStr(self):
|
||
"""返回 UUID 字符串形式"""
|
||
return UUIDObj(self.UserUUID).str
|
||
|
||
@property
|
||
def IsActive(self):
|
||
"""账户是否启用(UserAccountLicense == 1)"""
|
||
return self.UserAccountLicense == 1
|
||
|
||
@property
|
||
def IsOnJob(self):
|
||
"""是否在职(UserPositionStatus == 0)"""
|
||
return self.UserPositionStatus == 0
|
||
|
||
@property
|
||
def IsAnonymous(self):
|
||
return False
|
||
|
||
@property
|
||
def IsAuthenticated(self):
|
||
return True
|
||
|
||
@property
|
||
def PK(self):
|
||
return UUIDObj(self.UserUUID)
|
||
|
||
@property
|
||
def ID(self):
|
||
return UUIDObj(self.UserUUID)
|
||
|
||
def SetPassword(self, RawPassword):
|
||
"""设置密码——bcrypt 哈希"""
|
||
import bcrypt
|
||
salt = bcrypt.gensalt(rounds=12)
|
||
hashed = bcrypt.hashpw(RawPassword.encode('utf-8'), salt)
|
||
self.UserPassword = hashed.decode('utf-8')
|
||
|
||
def CheckPassword(self, RawPassword):
|
||
"""验证密码——兼容 bcrypt 哈希和明文存储"""
|
||
import bcrypt
|
||
stored = self.UserPassword or ''
|
||
# bcrypt 哈希以 $2b$ 或 $2a$ 开头,长度固定 60
|
||
if stored.startswith(('$2b$', '$2a$')) and len(stored) == 60:
|
||
try:
|
||
return bcrypt.checkpw(
|
||
RawPassword.encode('utf-8'),
|
||
stored.encode('utf-8')
|
||
)
|
||
except Exception:
|
||
return False
|
||
else:
|
||
# 明文密码(旧数据兼容)
|
||
return stored == RawPassword
|
||
|
||
def GetTele(self):
|
||
"""获取电话,空则返回默认文本"""
|
||
return self.UserTele or '未填写'
|
||
|
||
def GetCompanies(self):
|
||
"""获取用户所属公司列表"""
|
||
# Company 模型不在 gvsdsdk 中,此处返回空 QuerySet
|
||
# 如需使用,请在子服务器中自行实现
|
||
return []
|
||
|
||
def GetRoles(self):
|
||
"""获取用户角色列表(需在同一 database 连接下使用)"""
|
||
user_role_uuids = UserRole.objects.filter(
|
||
UserUUID=self.UserUUID
|
||
).values_list('RoleUUID', flat=True)
|
||
return Role.objects.filter(RoleUUID__in=user_role_uuids)
|
||
|
||
def GetDepartments(self):
|
||
"""获取用户部门列表"""
|
||
# Department 模型不在 gvsdsdk 中,此处返回空列表
|
||
# 如需使用,请在子服务器中自行实现
|
||
return []
|
||
|
||
def HasPermission(self, PermCode):
|
||
"""检查用户是否拥有指定权限(需在同一 database 连接下使用)
|
||
|
||
通过 UserRole → RolePermission → Permission 链路查询,
|
||
汇总所有角色的操作权限后判断。
|
||
"""
|
||
user_role_uuids = UserRole.objects.filter(
|
||
UserUUID=self.UserUUID
|
||
).values_list('RoleUUID', flat=True)
|
||
perm_uuids = RolePermission.objects.filter(
|
||
RoleUUID__in=user_role_uuids
|
||
).values_list('PermUUID', flat=True)
|
||
return Permission.objects.filter(
|
||
PermUUID__in=perm_uuids,
|
||
PermCode=PermCode,
|
||
PermStatus=1,
|
||
).exists()
|
||
|
||
# Django auth 框架兼容(内部使用蛇形命名)
|
||
@property
|
||
def is_active(self):
|
||
return self.IsActive
|
||
|
||
@property
|
||
def is_anonymous(self):
|
||
return self.IsAnonymous
|
||
|
||
@property
|
||
def is_authenticated(self):
|
||
return self.IsAuthenticated
|
||
|
||
@property
|
||
def id(self):
|
||
"""兼容旧代码的 id 属性,返回 UserUUID"""
|
||
return self.UserUUID
|
||
|
||
@property
|
||
def yonghuid(self):
|
||
"""兼容旧代码的 yonghuid 属性"""
|
||
return self.UserUID
|
||
|
||
@property
|
||
def openid(self):
|
||
"""兼容旧代码的 openid 属性"""
|
||
return self.OpenID
|
||
|
||
@property
|
||
def user_type(self):
|
||
"""兼容旧代码的 user_type 属性 — 通过 UserRole 推断(带实例缓存)"""
|
||
# 实例缓存:同一请求内只查一次
|
||
if hasattr(self, '_cached_user_type'):
|
||
return self._cached_user_type
|
||
try:
|
||
# 单次查询获取所有角色名
|
||
role_uuids = UserRole.objects.filter(
|
||
UserUUID=self.UserUUID
|
||
).values_list('RoleUUID', flat=True)
|
||
role_names = set(
|
||
Role.objects.filter(RoleUUID__in=role_uuids)
|
||
.values_list('RoleName', flat=True)
|
||
)
|
||
# 优先级:管理员 > 客服 > 负责人 > 组长 > 审核官 > 商家 > 打手
|
||
for rn in role_names:
|
||
if '管理员' in rn or 'admin' in rn.lower():
|
||
self._cached_user_type = 'admin'
|
||
return 'admin'
|
||
for rn in role_names:
|
||
if '客服' in rn or 'kefu' in rn.lower():
|
||
self._cached_user_type = 'kefu'
|
||
return 'kefu'
|
||
for rn in role_names:
|
||
if '负责人' in rn or '管事' in rn or 'guanshi' in rn.lower():
|
||
self._cached_user_type = 'guanshi'
|
||
return 'guanshi'
|
||
for rn in role_names:
|
||
if '组长' in rn or 'zuzhang' in rn.lower():
|
||
self._cached_user_type = 'zuzhang'
|
||
return 'zuzhang'
|
||
for rn in role_names:
|
||
if '审核' in rn or 'shenheguan' in rn.lower():
|
||
self._cached_user_type = 'shenheguan'
|
||
return 'shenheguan'
|
||
for rn in role_names:
|
||
if '商家' in rn or 'shangjia' in rn.lower():
|
||
self._cached_user_type = 'shop'
|
||
return 'shop'
|
||
for rn in role_names:
|
||
if '打手' in rn or 'dashou' in rn.lower():
|
||
self._cached_user_type = 'dashou'
|
||
return 'dashou'
|
||
self._cached_user_type = 'normal'
|
||
return 'normal'
|
||
except Exception:
|
||
self._cached_user_type = 'normal'
|
||
return 'normal'
|
||
|
||
@property
|
||
def pk(self):
|
||
return self.PK
|
||
|
||
@property
|
||
def id(self):
|
||
return self.ID
|
||
|
||
@property
|
||
def shoujihao_renzheng(self):
|
||
"""兼容旧代码 — 手机号是否已认证"""
|
||
return self.PhoneVerified
|
||
|
||
@shoujihao_renzheng.setter
|
||
def shoujihao_renzheng(self, value):
|
||
self.PhoneVerified = bool(value)
|
||
|
||
# ── 旧字段兼容属性(UserMain → gvsdsdk.User 映射)──
|
||
|
||
@property
|
||
def avatar(self):
|
||
return self.Avatar
|
||
|
||
@avatar.setter
|
||
def avatar(self, value):
|
||
self.Avatar = value
|
||
|
||
@property
|
||
def phone(self):
|
||
return self.Phone
|
||
|
||
@phone.setter
|
||
def phone(self, value):
|
||
self.Phone = value
|
||
|
||
@property
|
||
def ip(self):
|
||
return self.IP
|
||
|
||
@ip.setter
|
||
def ip(self, value):
|
||
self.IP = value
|
||
|
||
@property
|
||
def last_login_time(self):
|
||
return self.UserLastLoginDate
|
||
|
||
@last_login_time.setter
|
||
def last_login_time(self, value):
|
||
self.UserLastLoginDate = value
|
||
|
||
@property
|
||
def zhifu(self):
|
||
"""兼容旧代码 — 收款码图片(存于 UserSensitiveData)"""
|
||
try:
|
||
return self.sensitive_data.zhifu
|
||
except Exception:
|
||
return ''
|
||
|
||
@zhifu.setter
|
||
def zhifu(self, value):
|
||
try:
|
||
sd = self.sensitive_data
|
||
sd.zhifu = value
|
||
sd.save(update_fields=['zhifu'])
|
||
except Exception:
|
||
pass
|
||
|
||
@property
|
||
def skzhanghao(self):
|
||
"""兼容旧代码 — 收款账号(存于 UserSensitiveData)"""
|
||
try:
|
||
return self.sensitive_data.skzhanghao
|
||
except Exception:
|
||
return ''
|
||
|
||
@skzhanghao.setter
|
||
def skzhanghao(self, value):
|
||
try:
|
||
sd = self.sensitive_data
|
||
sd.skzhanghao = value
|
||
sd.save(update_fields=['skzhanghao'])
|
||
except Exception:
|
||
pass
|
||
|
||
def set_password(self, raw_password):
|
||
self.SetPassword(raw_password)
|
||
|
||
def check_password(self, raw_password):
|
||
return self.CheckPassword(raw_password)
|
||
|
||
@property
|
||
def dashou_profile(self):
|
||
"""兼容旧代码"""
|
||
from users.models import UserDashou
|
||
return UserDashou.objects.filter(user=self).first()
|
||
|
||
@property
|
||
def shop_profile(self):
|
||
"""兼容旧代码"""
|
||
from users.models import UserShangjia
|
||
return UserShangjia.objects.filter(user=self).first()
|
||
|
||
@property
|
||
def guanshi_profile(self):
|
||
"""兼容旧代码"""
|
||
from users.models import UserGuanshi
|
||
return UserGuanshi.objects.filter(user=self).first()
|
||
|
||
@property
|
||
def boss_profile(self):
|
||
"""兼容旧代码"""
|
||
from users.models import UserBoss
|
||
return UserBoss.objects.filter(user=self).first()
|
||
|
||
@property
|
||
def admin_profile(self):
|
||
"""兼容旧代码"""
|
||
from users.models import AdminProfile
|
||
return AdminProfile.objects.filter(user=self).first()
|
||
|
||
@property
|
||
def kefu_profile(self):
|
||
"""兼容旧代码"""
|
||
from users.models import UserKefu
|
||
return UserKefu.objects.filter(user=self).first()
|
||
|
||
@property
|
||
def zuzhang_profile(self):
|
||
"""兼容旧代码"""
|
||
from users.models import UserZuzhang
|
||
return UserZuzhang.objects.filter(user=self).first()
|
||
|
||
@property
|
||
def shenheguan_profile(self):
|
||
"""兼容旧代码"""
|
||
from users.models import UserShenheguan
|
||
return UserShenheguan.objects.filter(user=self).first()
|
||
|
||
def has_role(self, role_code):
|
||
"""检查用户是否拥有指定角色(支持角色名或旧 user_type 值)"""
|
||
# 角色名映射:旧 user_type → 角色名
|
||
ROLE_CODE_MAP = {
|
||
'admin': '管理员', 'kefu': '客服', 'guanshi': '负责人',
|
||
'zuzhang': '组长', 'shenheguan': '审核官', 'shop': '商家',
|
||
'dashou': '打手', 'normal': '消费者',
|
||
}
|
||
target = ROLE_CODE_MAP.get(role_code, role_code)
|
||
try:
|
||
role_uuids = UserRole.objects.filter(
|
||
UserUUID=self.UserUUID
|
||
).values_list('RoleUUID', flat=True)
|
||
return Role.objects.filter(
|
||
RoleUUID__in=role_uuids, RoleName=target
|
||
).exists()
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
# === 用户头像表 ===
|
||
|
||
class UserAvatar(QModel):
|
||
"""用户头像表(映射 SAAS 主库 UserAvatar 表)"""
|
||
|
||
UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID')
|
||
UserAvatar = models.BinaryField(editable=True, db_column='UserAvatar', verbose_name='头像数据')
|
||
AvatarUploadTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='AvatarUploadTime', verbose_name='上传时间')
|
||
AvatarFileType = models.CharField(max_length=16, default='png', db_column='AvatarFileType', verbose_name='文件类型')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'UserAvatar'
|
||
verbose_name = '用户头像'
|
||
verbose_name_plural = '用户头像'
|
||
|
||
|
||
# === 用户双因素认证表 ===
|
||
|
||
class User2FA(QModel):
|
||
"""用户双因素认证表(映射 SAAS 主库 User2FA 表)"""
|
||
|
||
User2FAUUID = UUIDField(primary_key=True, db_column='User2FAUUID', verbose_name='2FA UUID')
|
||
UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID')
|
||
SecretKey = models.CharField(max_length=64, db_column='SecretKey', verbose_name='密钥')
|
||
IsEnabled = models.SmallIntegerField(default=0, db_column='IsEnabled', verbose_name='是否启用')
|
||
Verified = models.SmallIntegerField(default=0, db_column='Verified', verbose_name='已验证')
|
||
LastUsedAt = models.DateTimeField(null=True, blank=True, db_column='LastUsedAt', verbose_name='最后使用')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'User2FA'
|
||
verbose_name = '双因素认证'
|
||
verbose_name_plural = '双因素认证'
|
||
|
||
|
||
# === 双因素认证待验证表 ===
|
||
|
||
class UserPending2FA(QModel):
|
||
"""双因素认证待验证表(映射 SAAS 主库 UserPending2FA 表)"""
|
||
|
||
PendingUUID = UUIDField(primary_key=True, db_column='PendingUUID', verbose_name='待确认UUID')
|
||
UserUUID = UUIDField(unique=True, db_column='UserUUID', verbose_name='用户UUID')
|
||
SecretKey = models.CharField(max_length=64, db_column='SecretKey', verbose_name='密钥')
|
||
ExpireAt = models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'UserPending2FA'
|
||
verbose_name = '2FA待确认'
|
||
verbose_name_plural = '2FA待确认'
|
||
|
||
|
||
# === 临时令牌表 ===
|
||
|
||
def _EltEndTimeDefault():
|
||
from django.conf import settings
|
||
days = getattr(settings, 'ELT_TOKEN_LIFETIME_DAYS', 30)
|
||
return _datetime.datetime.utcnow() + _datetime.timedelta(days=days)
|
||
|
||
|
||
class EphemeralToken(QModel):
|
||
"""临时令牌表——用于 API 认证与会话管理(映射 SAAS 主库 EphemeralToken 表)"""
|
||
|
||
ELTUUID = UUIDField(primary_key=True, db_column='ELTUUID', verbose_name='令牌UUID')
|
||
ELTUserUUID = UUIDField(db_index=True, db_column='ELTUserUUID', verbose_name='用户UUID')
|
||
ELTCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='ELTCreateTime', verbose_name='创建时间')
|
||
ELTEndTime = models.DateTimeField(default=_EltEndTimeDefault, db_column='ELTEndTime', verbose_name='过期时间')
|
||
ELTType = models.SmallIntegerField(default=0, db_column='ELTType', verbose_name='令牌类型')
|
||
ELTAllowIP = models.JSONField(default=list, db_column='ELTAllowIP', verbose_name='允许IP')
|
||
ELTStatus = models.SmallIntegerField(default=1, db_index=True, db_column='ELTStatus', verbose_name='状态')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'EphemeralToken'
|
||
verbose_name = '临时令牌'
|
||
verbose_name_plural = '临时令牌'
|
||
|
||
def IsExpired(self):
|
||
"""检查令牌是否过期"""
|
||
return _datetime.datetime.utcnow() > self.ELTEndTime.replace(tzinfo=None)
|
||
|
||
def IsValid(self):
|
||
"""检查令牌是否有效"""
|
||
return self.ELTStatus == 1 and not self.IsExpired()
|
||
|
||
|
||
# === 用户登录日志表 ===
|
||
|
||
class UserLoginLog(QModel):
|
||
"""用户登录日志表(映射 SAAS 主库 UserLoginLog 表)"""
|
||
|
||
LogUUID = UUIDField(primary_key=True, db_column='LogUUID', verbose_name='日志UUID')
|
||
UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID')
|
||
LoginIP = models.CharField(max_length=39, db_column='LoginIP', verbose_name='登录IP')
|
||
LoginPlatform = models.CharField(max_length=32, db_column='LoginPlatform', verbose_name='登录平台')
|
||
LoginStatus = models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态')
|
||
LoginTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='LoginTime', verbose_name='登录时间')
|
||
UserAgent = models.CharField(max_length=512, default='', db_column='UserAgent', verbose_name='用户代理')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'UserLoginLog'
|
||
verbose_name = '登录日志'
|
||
verbose_name_plural = '登录日志'
|
||
|
||
|
||
# === 账户激活码表 ===
|
||
|
||
class AccountActivation(QModel):
|
||
"""账户激活码表(映射 SAAS 主库 AccountActivation 表)"""
|
||
|
||
AccountActivationCode = FixedBinaryField(primary_key=True, binary_length=6, db_column='AccountActivationCode', verbose_name='激活码')
|
||
UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'AccountActivation'
|
||
verbose_name = '账户激活'
|
||
verbose_name_plural = '账户激活'
|
||
|
||
|
||
# === 角色表 ===
|
||
|
||
class Role(QModel):
|
||
"""角色表——权限分配单元(映射 SAAS 主库 Role 表)
|
||
|
||
角色是真正承载操作权限的实体,在单个租户内部独立存在,
|
||
不依附任何公司或部门。通过 AssignScope 控制角色的分配可见范围:
|
||
- 0:全租户管理员可分发
|
||
- 1:仅指定公司管理员可分发(AssignScopeUUID = CompanyUUID)
|
||
- 2:仅指定部门管理员可分发(AssignScopeUUID = DeptUUID)
|
||
"""
|
||
|
||
RoleUUID = UUIDField(primary_key=True, db_column='RoleUUID', verbose_name='角色UUID')
|
||
TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID')
|
||
RoleName = models.CharField(max_length=128, db_column='RoleName', verbose_name='角色名称')
|
||
RoleType = models.SmallIntegerField(default=2, db_column='RoleType', verbose_name='角色类型')
|
||
RoleStatus = models.SmallIntegerField(default=1, db_column='RoleStatus', verbose_name='角色状态')
|
||
AssignScope = models.SmallIntegerField(default=0, db_column='AssignScope', verbose_name='分配可见范围')
|
||
AssignScopeUUID = UUIDField(null=True, blank=True, db_column='AssignScopeUUID', verbose_name='分配范围UUID')
|
||
RoleDesc = models.CharField(max_length=512, default='', db_column='RoleDesc', verbose_name='角色描述')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'Role'
|
||
verbose_name = '角色'
|
||
verbose_name_plural = '角色'
|
||
|
||
def __str__(self):
|
||
return self.RoleName
|
||
|
||
def GetPermissions(self):
|
||
"""获取角色拥有的权限列表(通过 RolePermission 中间表)"""
|
||
perm_uuids = RolePermission.objects.filter(
|
||
RoleUUID=self.RoleUUID
|
||
).values_list('PermUUID', flat=True)
|
||
return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1)
|
||
|
||
def GetUsers(self):
|
||
"""获取拥有该角色的用户列表"""
|
||
user_uuids = UserRole.objects.filter(
|
||
RoleUUID=self.RoleUUID
|
||
).values_list('UserUUID', flat=True)
|
||
return User.objects.filter(UserUUID__in=user_uuids)
|
||
|
||
|
||
# === 权限表 ===
|
||
|
||
class Permission(QModel):
|
||
"""权限表——操作权限定义(映射 SAAS 主库 Permission 表)
|
||
|
||
独立的权限定义表,租户内全局可用,不再挂在角色下。
|
||
通过 RolePermission 中间表与角色建立多对多关系,
|
||
通过 PositionTemplatePermission 中间表与岗位模板建立多对多关系。
|
||
"""
|
||
|
||
PermUUID = UUIDField(primary_key=True, db_column='PermUUID', verbose_name='权限UUID')
|
||
TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID')
|
||
PermName = models.CharField(max_length=128, db_column='PermName', verbose_name='权限名称')
|
||
PermCode = models.CharField(max_length=128, db_column='PermCode', verbose_name='权限键')
|
||
PermCategory = models.CharField(max_length=64, default='', db_column='PermCategory', verbose_name='权限分类')
|
||
PermStatus = models.SmallIntegerField(default=1, db_column='PermStatus', verbose_name='是否启用')
|
||
PermDesc = models.CharField(max_length=512, default='', db_column='PermDesc', verbose_name='描述')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'Permission'
|
||
verbose_name = '权限'
|
||
verbose_name_plural = '权限'
|
||
|
||
def __str__(self):
|
||
return f"{self.PermName} ({self.PermCode})"
|
||
|
||
def GetRoles(self):
|
||
"""获取拥有该权限的角色列表"""
|
||
role_uuids = RolePermission.objects.filter(
|
||
PermUUID=self.PermUUID
|
||
).values_list('RoleUUID', flat=True)
|
||
return Role.objects.filter(RoleUUID__in=role_uuids)
|
||
|
||
|
||
# === 用户角色关联表 ===
|
||
|
||
class UserRole(QModel):
|
||
"""用户角色关联表——用户与角色的多对多映射(映射 SAAS 主库 UserRole 表)
|
||
|
||
通过 CompanyUUID 标识该用户-角色关联所属的公司,
|
||
用于菜单按公司差异化显示、数据权限按公司过滤等场景。
|
||
同一用户在不同公司下可绑定不同角色。
|
||
|
||
注意:UserUUID 和 RoleUUID 在 gvsds 中是 ForeignKey,
|
||
但此处改为 UUIDField 以保持独立性。
|
||
"""
|
||
|
||
UserRoleUUID = UUIDField(primary_key=True, db_column='UserRoleUUID', verbose_name='关联UUID')
|
||
# 原 gvsds 中为 ForeignKey(User),此处改为 UUIDField
|
||
UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID')
|
||
# 原 gvsds 中为 ForeignKey('Role'),此处改为 UUIDField
|
||
RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID')
|
||
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'UserRole'
|
||
verbose_name = '用户角色关联'
|
||
verbose_name_plural = '用户角色关联'
|
||
|
||
|
||
# === 角色权限关联表 ===
|
||
|
||
class RolePermission(QModel):
|
||
"""角色权限关联表——角色与操作权限的多对多映射(映射 SAAS 主库 RolePermission 表)
|
||
|
||
一个角色可以拥有多个权限,一个权限也可以被多个角色引用。
|
||
角色和具体操作权限为多对多绑定关系,一套权限集合可以打包在一个角色内统一维护。
|
||
"""
|
||
|
||
RolePermUUID = UUIDField(primary_key=True, db_column='RolePermUUID', verbose_name='关联UUID')
|
||
RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID')
|
||
PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'RolePermission'
|
||
verbose_name = '角色权限关联'
|
||
verbose_name_plural = '角色权限关联'
|
||
|
||
|
||
# === 用户 ABAC 表 ===
|
||
|
||
class UserABAC(QModel):
|
||
"""用户 ABAC 表——基于属性的访问控制(映射 SAAS 主库 UserABAC 表)"""
|
||
|
||
UserABACUUID = UUIDField(primary_key=True, db_column='UserABACUUID', verbose_name='ABAC UUID')
|
||
UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID')
|
||
HierarchicalPermissionIdentifiers = models.CharField(max_length=256, db_column='HierarchicalPermissionIdentifiers', verbose_name='层级权限标识')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'UserABAC'
|
||
verbose_name = '用户ABAC'
|
||
verbose_name_plural = '用户ABAC'
|
||
|
||
|
||
# === 密码表 ===
|
||
|
||
class Password(QModel):
|
||
"""密码表——独立密码存储(映射 SAAS 主库 Password 表)"""
|
||
|
||
UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID')
|
||
Password = models.CharField(max_length=1024, db_column='Password', verbose_name='密码哈希')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'Password'
|
||
verbose_name = '密码'
|
||
verbose_name_plural = '密码'
|
||
|
||
|
||
# === 商铺表 ===
|
||
|
||
class Shop(QModel):
|
||
"""商铺表——SAAS 多商铺体系中的商铺实体
|
||
|
||
每个商铺属于一个公司(CompanyUUID),可关联多个角色(通过 RoleShop)。
|
||
"""
|
||
|
||
ShopUUID = UUIDField(primary_key=True, db_column='ShopUUID', verbose_name='商铺UUID')
|
||
CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')
|
||
ShopName = models.CharField(max_length=128, db_column='ShopName', verbose_name='商铺名称')
|
||
ShopCode = models.CharField(max_length=64, default='', db_column='ShopCode', verbose_name='商铺编码')
|
||
ShopType = models.SmallIntegerField(default=0, db_column='ShopType', verbose_name='商铺类型')
|
||
ShopStatus = models.SmallIntegerField(default=1, db_column='ShopStatus', verbose_name='商铺状态')
|
||
ShopAddress = models.CharField(max_length=512, default='', db_column='ShopAddress', verbose_name='商铺地址')
|
||
ShopContact = models.CharField(max_length=128, default='', db_column='ShopContact', verbose_name='联系人')
|
||
ShopPhone = models.CharField(max_length=32, default='', db_column='ShopPhone', verbose_name='联系电话')
|
||
ShopDesc = models.CharField(max_length=512, default='', db_column='ShopDesc', verbose_name='描述')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'Shop'
|
||
verbose_name = '商铺'
|
||
verbose_name_plural = '商铺'
|
||
|
||
def __str__(self):
|
||
return self.ShopName
|
||
|
||
def GetRoles(self):
|
||
"""获取商铺关联的所有角色"""
|
||
role_uuids = RoleShop.objects.filter(
|
||
ShopUUID=self.ShopUUID
|
||
).values_list('RoleUUID', flat=True)
|
||
return Role.objects.filter(RoleUUID__in=role_uuids)
|
||
|
||
|
||
# === 角色商铺关联表 ===
|
||
|
||
class RoleShop(QModel):
|
||
"""角色商铺关联表——角色与商铺的多对多映射
|
||
|
||
一个角色可以管理多个商铺,一个商铺也可以被多个角色管理。
|
||
"""
|
||
|
||
RoleShopUUID = UUIDField(primary_key=True, db_column='RoleShopUUID', verbose_name='关联UUID')
|
||
RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID')
|
||
ShopUUID = UUIDField(db_column='ShopUUID', verbose_name='商铺UUID')
|
||
IsMainShop = models.SmallIntegerField(default=0, db_column='IsMainShop', verbose_name='主商铺')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'RoleShop'
|
||
verbose_name = '角色商铺关联'
|
||
verbose_name_plural = '角色商铺关联'
|
||
|
||
|
||
# === 岗位模板表 ===
|
||
|
||
class PositionTemplate(QModel):
|
||
"""岗位模板表——权限初始化快照模板(映射 SAAS 主库 PositionTemplate 表)
|
||
|
||
租户内全局可重复使用,定位是权限初始化快照模板,
|
||
不直接分配给员工、不参与运行时鉴权。
|
||
新建角色时可一键复制模板内的全部权限内容生成新角色。
|
||
模板后续调整只会作用于之后新创建的角色,
|
||
已有角色不会同步变更,实现模板复用和存量权限独立管控互不干扰。
|
||
"""
|
||
|
||
PositionTemplateUUID = UUIDField(primary_key=True, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')
|
||
TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID')
|
||
TemplateName = models.CharField(max_length=128, db_column='TemplateName', verbose_name='模板名称')
|
||
TemplateCode = models.CharField(max_length=64, default='', db_column='TemplateCode', verbose_name='模板编码')
|
||
TemplateStatus = models.SmallIntegerField(default=1, db_column='TemplateStatus', verbose_name='模板状态')
|
||
TemplateDesc = models.CharField(max_length=512, default='', db_column='TemplateDesc', verbose_name='模板描述')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'PositionTemplate'
|
||
verbose_name = '岗位模板'
|
||
verbose_name_plural = '岗位模板'
|
||
|
||
def __str__(self):
|
||
return self.TemplateName
|
||
|
||
def GetPermissions(self):
|
||
"""获取模板关联的权限列表"""
|
||
perm_uuids = PositionTemplatePermission.objects.filter(
|
||
PositionTemplateUUID=self.PositionTemplateUUID
|
||
).values_list('PermUUID', flat=True)
|
||
return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1)
|
||
|
||
def CopyToRole(self, RoleName, TenantUUID):
|
||
"""从模板复制权限到新角色(一次性复制,后续模板变更不影响已有角色)"""
|
||
role = Role.objects.create(
|
||
RoleUUID=_uuid.uuid4().bytes,
|
||
TenantUUID=TenantUUID,
|
||
RoleName=RoleName,
|
||
)
|
||
template_perms = PositionTemplatePermission.objects.filter(
|
||
PositionTemplateUUID=self.PositionTemplateUUID
|
||
)
|
||
for tp in template_perms:
|
||
RolePermission.objects.create(
|
||
RolePermUUID=_uuid.uuid4().bytes,
|
||
RoleUUID=role.RoleUUID,
|
||
PermUUID=tp.PermUUID,
|
||
)
|
||
return role
|
||
|
||
|
||
# === 岗位模板权限关联表 ===
|
||
|
||
class PositionTemplatePermission(QModel):
|
||
"""岗位模板权限关联表——岗位模板与操作权限的多对多映射
|
||
|
||
仅作为复制源头,不参与运行时鉴权。
|
||
新建角色时一键复制模板权限到角色,复制后模板与角色无运行时绑定。
|
||
"""
|
||
|
||
PTPermUUID = UUIDField(primary_key=True, db_column='PTPermUUID', verbose_name='关联UUID')
|
||
PositionTemplateUUID = UUIDField(db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')
|
||
PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'PositionTemplatePermission'
|
||
verbose_name = '岗位模板权限关联'
|
||
verbose_name_plural = '岗位模板权限关联'
|
||
|
||
|
||
# === 人事职务标签表 ===
|
||
|
||
class Position(QModel):
|
||
"""人事职务标签表——仅用于人事管理标注(映射 SAAS 主库 Position 表)
|
||
|
||
员工会标注对应的职务标签,该标签仅为人事标注信息,
|
||
不直接绑定权限,和角色、权限体系完全解耦。
|
||
人员调岗、升职只修改组织归属和人事职务标签,权限只需增删绑定角色即可,互不干扰。
|
||
"""
|
||
|
||
PositionUUID = UUIDField(primary_key=True, db_column='PositionUUID', verbose_name='职务UUID')
|
||
TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID')
|
||
PositionName = models.CharField(max_length=128, db_column='PositionName', verbose_name='职务名称')
|
||
PositionCode = models.CharField(max_length=64, default='', db_column='PositionCode', verbose_name='职务编码')
|
||
PositionLevel = models.SmallIntegerField(default=0, db_column='PositionLevel', verbose_name='职务级别')
|
||
PositionStatus = models.SmallIntegerField(default=1, db_column='PositionStatus', verbose_name='状态')
|
||
PositionDesc = models.CharField(max_length=512, default='', db_column='PositionDesc', verbose_name='描述')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'Position'
|
||
verbose_name = '人事职务标签'
|
||
verbose_name_plural = '人事职务标签'
|
||
|
||
def __str__(self):
|
||
return self.PositionName
|
||
|
||
|
||
# === 员工部门关联表 ===
|
||
|
||
class UserDept(QModel):
|
||
"""员工部门关联表——员工与部门的多对多映射(映射 SAAS 主库 UserDept 表)
|
||
|
||
支持一人多部门兼职,同时标注该员工在对应部门的职务标签。
|
||
该层级仅用于组织架构展示、审批流转、人员统计,
|
||
同时为数据权限判断提供人员所属组织属性依据,不参与操作权限校验。
|
||
"""
|
||
|
||
UserDeptUUID = UUIDField(primary_key=True, db_column='UserDeptUUID', verbose_name='关联UUID')
|
||
UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID')
|
||
DeptUUID = UUIDField(db_column='DeptUUID', verbose_name='部门UUID')
|
||
PositionUUID = UUIDField(null=True, blank=True, db_column='PositionUUID', verbose_name='职务标签UUID')
|
||
IsMainDept = models.SmallIntegerField(default=1, db_column='IsMainDept', verbose_name='是否主部门')
|
||
CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间')
|
||
|
||
class Meta:
|
||
managed = True
|
||
app_label = 'gvsdsdk'
|
||
db_table = 'UserDept'
|
||
verbose_name = '员工部门关联'
|
||
verbose_name_plural = '员工部门关联'
|