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": }', ) 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.")