316 lines
9.6 KiB
Python
316 lines
9.6 KiB
Python
from django.db import models
|
||
|
||
from core.enums import (
|
||
PermissionChangeAction,
|
||
PermissionChangeTargetType,
|
||
PermissionDataScopeType,
|
||
PermissionOverrideMode,
|
||
)
|
||
from core.models.base import TimeStampedModel, UUIDPrimaryKeyModel
|
||
|
||
|
||
class StaffRole(UUIDPrimaryKeyModel):
|
||
staff = models.ForeignKey(
|
||
"org.Staff",
|
||
on_delete=models.CASCADE,
|
||
related_name="staff_roles",
|
||
verbose_name="所属员工",
|
||
help_text="员工删除时级联删除角色关联",
|
||
)
|
||
role = models.ForeignKey(
|
||
"fonrey_permission.Role",
|
||
on_delete=models.PROTECT,
|
||
related_name="staff_links",
|
||
verbose_name="角色",
|
||
help_text="角色被员工引用时禁止删除",
|
||
)
|
||
is_primary = models.BooleanField(
|
||
default=False,
|
||
verbose_name="是否主角色",
|
||
help_text="每个员工有且仅有一个主角色",
|
||
)
|
||
assigned_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="分配时间",
|
||
)
|
||
assigned_by = models.ForeignKey(
|
||
"org.Staff",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="staff_role_assignments_made",
|
||
verbose_name="分配操作人",
|
||
)
|
||
valid_from = models.DateField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="生效日",
|
||
help_text='预留未来「定时生效」功能',
|
||
)
|
||
valid_until = models.DateField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="失效日",
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "staff_roles"
|
||
verbose_name = "员工角色"
|
||
verbose_name_plural = "员工角色"
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["staff", "role"],
|
||
name="uq_staff_roles",
|
||
),
|
||
models.UniqueConstraint(
|
||
fields=["staff"],
|
||
condition=models.Q(is_primary=True),
|
||
name="uq_staff_roles_primary",
|
||
),
|
||
]
|
||
indexes = [
|
||
models.Index(fields=["role"], name="idx_staff_roles_role"),
|
||
]
|
||
|
||
def __str__(self) -> str:
|
||
marker = " [primary]" if self.is_primary else ""
|
||
return f"{self.staff_id} → {self.role_id}{marker}"
|
||
|
||
|
||
class StaffPermissionOverride(UUIDPrimaryKeyModel):
|
||
staff = models.ForeignKey(
|
||
"org.Staff",
|
||
on_delete=models.CASCADE,
|
||
related_name="permission_overrides",
|
||
verbose_name="所属员工",
|
||
help_text="员工删除时级联删除覆盖记录",
|
||
)
|
||
permission_def = models.ForeignKey(
|
||
"fonrey_permission.PermissionDef",
|
||
on_delete=models.PROTECT,
|
||
related_name="staff_overrides",
|
||
verbose_name="被覆盖权限项",
|
||
)
|
||
value = models.JSONField(
|
||
verbose_name="个人权限值",
|
||
help_text='统一格式 {"v": <value>}',
|
||
)
|
||
override_mode = models.CharField(
|
||
max_length=10,
|
||
choices=PermissionOverrideMode.choices,
|
||
default=PermissionOverrideMode.REPLACE,
|
||
verbose_name="覆盖模式",
|
||
help_text="REPLACE=替换合并值 / RESTRICT=限制上限 / GRANT=仅扩展",
|
||
)
|
||
reason = models.TextField(
|
||
blank=True,
|
||
default="",
|
||
verbose_name="备注",
|
||
help_text="管理员备注,建议强制填写以便审计",
|
||
)
|
||
modified_by = models.ForeignKey(
|
||
"org.Staff",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="staff_overrides_modified",
|
||
verbose_name="修改人",
|
||
)
|
||
modified_at = models.DateTimeField(
|
||
auto_now=True,
|
||
verbose_name="最近修改时间",
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "staff_permission_overrides"
|
||
verbose_name = "个人权限覆盖"
|
||
verbose_name_plural = "个人权限覆盖"
|
||
constraints = [
|
||
models.UniqueConstraint(
|
||
fields=["staff", "permission_def"],
|
||
name="uq_staff_overrides",
|
||
),
|
||
]
|
||
indexes = [
|
||
models.Index(fields=["staff"], name="idx_staff_overrides_staff"),
|
||
]
|
||
|
||
|
||
class StaffDataScope(UUIDPrimaryKeyModel):
|
||
staff = models.ForeignKey(
|
||
"org.Staff",
|
||
on_delete=models.CASCADE,
|
||
related_name="data_scopes",
|
||
verbose_name="所属员工",
|
||
help_text="员工删除时级联删除范围记录",
|
||
)
|
||
scope_type = models.CharField(
|
||
max_length=20,
|
||
choices=PermissionDataScopeType.choices,
|
||
verbose_name="范围类型",
|
||
help_text="self=本人 / group=本组 / store=本门店 / area=本区域 / region=本大区 / company=全公司 / custom_unit=指定节点",
|
||
)
|
||
org_unit = models.ForeignKey(
|
||
"org.OrgUnit",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.PROTECT,
|
||
related_name="data_scope_grants",
|
||
verbose_name="组织节点",
|
||
help_text="scope_type=custom_unit 时必填,其他类型为 NULL",
|
||
)
|
||
is_readable = models.BooleanField(
|
||
default=True,
|
||
verbose_name="可读",
|
||
)
|
||
is_writable = models.BooleanField(
|
||
default=False,
|
||
verbose_name="可写",
|
||
help_text="默认只读",
|
||
)
|
||
granted_by = models.ForeignKey(
|
||
"org.Staff",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="data_scopes_granted",
|
||
verbose_name="授权操作人",
|
||
)
|
||
granted_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="授权时间",
|
||
)
|
||
expires_at = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="临时授权失效时间",
|
||
)
|
||
reason = models.TextField(
|
||
blank=True,
|
||
default="",
|
||
verbose_name="授予原因",
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "staff_data_scopes"
|
||
verbose_name = "员工数据范围"
|
||
verbose_name_plural = "员工数据范围"
|
||
indexes = [
|
||
models.Index(fields=["staff"], name="idx_data_scopes_staff"),
|
||
models.Index(fields=["org_unit"], name="idx_data_scopes_org"),
|
||
models.Index(
|
||
fields=["expires_at"],
|
||
name="idx_data_scopes_expires",
|
||
condition=models.Q(expires_at__isnull=False),
|
||
),
|
||
]
|
||
|
||
|
||
class PermissionChangeLog(UUIDPrimaryKeyModel):
|
||
target_type = models.CharField(
|
||
max_length=30,
|
||
choices=PermissionChangeTargetType.choices,
|
||
verbose_name="变更对象类型",
|
||
help_text="role / role_permission / staff_role / staff_override / staff_scope",
|
||
)
|
||
target_id = models.UUIDField(
|
||
verbose_name="变更对象 ID",
|
||
)
|
||
staff = models.ForeignKey(
|
||
"org.Staff",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="permission_change_logs_affecting",
|
||
verbose_name="被影响员工",
|
||
help_text="target 是 staff_role/staff_override/staff_scope 时必填",
|
||
)
|
||
role = models.ForeignKey(
|
||
"fonrey_permission.Role",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="change_logs",
|
||
verbose_name="被影响角色",
|
||
)
|
||
permission_code = models.CharField(
|
||
max_length=150,
|
||
blank=True,
|
||
default="",
|
||
verbose_name="权限编码",
|
||
help_text="用 code 而非 FK,避免 PermissionDef 删除后日志丢失",
|
||
)
|
||
action = models.CharField(
|
||
max_length=20,
|
||
choices=PermissionChangeAction.choices,
|
||
verbose_name="操作动作",
|
||
help_text="create / update / delete / assign / revoke",
|
||
)
|
||
old_value = models.JSONField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="变更前快照",
|
||
)
|
||
new_value = models.JSONField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="变更后快照",
|
||
)
|
||
operator = models.ForeignKey(
|
||
"org.Staff",
|
||
on_delete=models.PROTECT,
|
||
related_name="permission_changes_operated",
|
||
verbose_name="操作人",
|
||
)
|
||
operator_ip = models.GenericIPAddressField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="操作来源 IP",
|
||
)
|
||
user_agent = models.TextField(
|
||
blank=True,
|
||
default="",
|
||
verbose_name="操作终端 UA",
|
||
)
|
||
reason = models.TextField(
|
||
blank=True,
|
||
default="",
|
||
verbose_name="操作原因",
|
||
help_text="批量设置角色等场景强制填写",
|
||
)
|
||
operated_at = models.DateTimeField(
|
||
auto_now_add=True,
|
||
verbose_name="操作时间",
|
||
help_text="append-only 流水,分区键",
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "permission_change_logs"
|
||
verbose_name = "权限变更流水"
|
||
verbose_name_plural = "权限变更流水"
|
||
ordering = ["-operated_at"]
|
||
indexes = [
|
||
models.Index(
|
||
fields=["staff", "-operated_at"],
|
||
name="idx_perm_log_staff",
|
||
condition=models.Q(staff__isnull=False),
|
||
),
|
||
models.Index(
|
||
fields=["role", "-operated_at"],
|
||
name="idx_perm_log_role",
|
||
condition=models.Q(role__isnull=False),
|
||
),
|
||
models.Index(
|
||
fields=["target_type", "target_id", "-operated_at"],
|
||
name="idx_perm_log_target",
|
||
),
|
||
models.Index(
|
||
fields=["operator", "-operated_at"],
|
||
name="idx_perm_log_operator",
|
||
),
|
||
models.Index(fields=["-operated_at"], name="idx_perm_log_time"),
|
||
]
|
||
|
||
def delete(self, *args, **kwargs):
|
||
raise NotImplementedError("PermissionChangeLog is append-only and cannot be deleted.")
|