权限管理设计方案

This commit is contained in:
2026-04-24 16:52:36 +08:00
parent 0d6f30a55a
commit 207d6e8b42
4 changed files with 681 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

View File

@@ -41,11 +41,11 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-ten
### 1.2 目标用户
| 角色 | 使用场景 | 频率 |
|------|---------|------|
| 角色 | 使用场景 | 频率 |
| --------------------------- | ----------- | ------ |
| 超级管理员Platform Super Admin | 全局配置、高危操作授权 | 低频(每周) |
| 运人员Ops Operator | 日常租户管理、监控巡检 | 高频(每日) |
| 只读审计员Read-only Auditor | 日志查询、合规报告导出 | 中频(每周) |
| 运人员Ops Operator | 日常租户管理、监控巡检 | 高频(每日) |
| 只读审计员Read-only Auditor | 日志查询、合规报告导出 | 中频(每周) |
---

View File

@@ -0,0 +1,677 @@
# Fonrey 权限管理系统技术方案建议
**版本**: 1.0 | **项目**: Fonrey 房产经纪管理系统 | **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL + Redis
---
## 一、选型结论:为什么不用现有库
在回答五个核心需求之前,先明确**不推荐**使用哪些常见方案:
|方案|为何不适用|
|---|---|
|`django.contrib.auth` 原生权限|权限值只有 Boolean不支持范围型枚举和数值型|
|`django-guardian`|面向行级Row-level权限是对象权限模型与本项目需求错位|
|`django-rules`|基于函数规则,适合纯 Python 逻辑判断,无法存储枚举/数字值|
|Casbin / OPA|重型策略引擎,引入额外运维复杂度,与 `django-tenants` 集成困难|
**推荐方案:自定义 RBAC + 个人覆盖层Hybrid Permission Model**
这是唯一能同时满足五个需求的路径,实现量适中,完全在 Django ORM 生态内,与 `django-tenants` 天然兼容。
![[Pasted image 20260424165102.png]]
以下是方案的核心要点汇总:
**为什么不用现有库**Django 原生权限只支持 Boolean`django-guardian` 是行级权限模型,两者都无法满足你的「范围型枚举 + 数值型」需求,因此推荐完全自定义实现。
**五个需求对应的设计决策**
**① 基于角色分配权限** — 标准 RBAC 三表:`PermissionDef`(权限目录,开发者维护,放 shared schema`Role`(角色模板)→ `RolePermission`(角色 × 权限 → 值)
**② 个人权限调整** — 增加 `StaffPermissionOverride` 表,**稀疏覆盖设计**:只存储与角色默认值不同的项,不复制全量权限。解析优先级:个人覆盖 > 角色合并 > 系统默认。
**③ 多种值类型** — 统一用 `JSONField` 存储,格式统一为 `{"v": value}``PermissionDef.value_type` 标记类型(`BOOLEAN`/`SCOPE`/`INTEGER`),前端根据类型渲染 Toggle/下拉/数字框。范围型的可选枚举(如某权限只有三档选项)存在 `scope_choices` JSON 数组中。
**④ 多角色叠加规则** — 采用**最宽松原则**PRD 6.4 倾向确认):`BOOLEAN``OR``SCOPE` 取枚举最大值(本人 < 本组 < 本门店 < 本区域 < 全公司)、`INTEGER` 取最大值(`0=不限制`是最宽松值)。
**⑤ 契合技术栈** — Redis 缓存员工权限快照(变更时主动 `invalidate`HTMX 按模块懒加载权限面板(避免一次渲染 300+ 条Alpine.js 管理 Toggle/下拉本地状态;与 `django-tenants` 的集成方式:`PermissionDef``shared_apps`,其余所有表进 `tenant_apps`
文档中还包含完整的 Model 代码、权限解析引擎、缓存策略、HTMX 集成示例和「权限与角色不一致」标记逻辑的实现。
---
## 二、五个需求对应设计
### 需求 1基于角色来创建权限并分配RBAC 基础层)
采用标准 RBAC 三表结构:`PermissionDef`(权限定义)→ `Role`(角色)→ `RolePermission`(角色权限值)。
- `PermissionDef` 是**权限目录**,由开发者维护,存储权限的元信息(名称、所属模块、值类型、可选范围等)
- `Role` 是**权限模板**,管理员在 UI 上创建,如"高级业务员"、"分行经理"
- `RolePermission` 存储角色对每个权限项的**具体配置值**
### 需求 2个人用户可在角色权限基础上再进行个性化调整个人覆盖层
增加 `StaffPermissionOverride` 表,只存储**与角色默认值不同的权限项**(稀疏覆盖,不复制全量)。
**解析优先级**:个人覆盖值 > 角色合并值 > 系统默认值
### 需求 3权限值涉及多种数据类型多态值存储
使用 `JSONField` 统一存储权限值,通过 `PermissionDef.value_type` 字段标记类型,前端根据类型渲染不同控件:
|`value_type`|存储格式|UI 控件|示例|
|---|---|---|---|
|`BOOLEAN`|`{"v": true}`|Toggle|是否显示今日新上房源|
|`SCOPE`|`{"v": "store"}`|下拉选择|查看私客范围(本人/本组/本门店/...|
|`INTEGER`|`{"v": 50}`|数字输入框|每日最多查看联系人数|
范围型SCOPE的**可选枚举值**在 `PermissionDef.scope_choices` 中定义JSON 数组),不同权限项的可选范围不同(如某项只有「本人/本门店/全公司」三档)。
### 需求 4多角色权限叠加规则并集/最宽松原则)
PRD 第 6 节已明确倾向:**取权限并集(最宽松原则)**。具体合并逻辑:
|值类型|合并规则|示例|
|---|---|---|
|`BOOLEAN`|`OR` — 任一角色开启则生效|角色A关闭、角色B开启 → 最终**开启**|
|`SCOPE`|`MAX` — 取范围最大的值|角色A=本组、角色B=本门店 → 最终**本门店**|
|`INTEGER`|`MAX` — 取最大数值;`0` 表示不限制,是最宽松值|角色A=20、角色B=50 → **50**角色A=20、角色B=0 → **0不限制**|
SCOPE 值的大小关系定义为:`无 < 本人 < 本组 < 本门店 < 本区域 < 全公司`
### 需求 5契合当前技术栈的实现方案
- 数据层PostgreSQL + `JSONField`,与 `django-tenants` Schema 隔离完全兼容
- 缓存层Redis 存储员工权限快照,变更时主动失效
- 视图层Django 视图 + HTMX 局部刷新权限编辑面板Alpine.js 管理 Toggle/下拉状态
- 权限校验:自定义 `permission_required` 装饰器 + Mixin替代 Django 原生权限系统
---
## 三、数据模型设计(完整)
```python
# apps/permissions/models.py
from django.db import models
from django.contrib.postgres.fields import ArrayField
class ScopeLevel(models.IntegerChoices):
"""范围型权限的枚举值,数值越大权限越宽"""
NONE = 0, ""
SELF = 1, "本人"
GROUP = 2, "本组"
STORE = 3, "本门店"
REGION = 4, "本区域"
COMPANY = 5, "全公司"
class ValueType(models.TextChoices):
BOOLEAN = "BOOLEAN", "开关型"
SCOPE = "SCOPE", "范围型"
INTEGER = "INTEGER", "数值型"
class PermissionModule(models.TextChoices):
HOME = "home", "首页"
PROPERTY = "property", "房源"
NEW_HOUSE = "new_house", "新房"
CLIENT = "client", "客源"
TRANSACTION = "transaction","交易"
DATA = "data", "数据"
MARKETING = "marketing", "营销"
HR = "hr", "人事OA"
CONTRACT = "contract", "合同"
TRINET = "trinet", "三网"
SYSTEM = "system", "系统"
MOBILE = "mobile", "移动端"
SMART_STORE = "smart_store","智能门店"
RECHARGE = "recharge", "在线充值"
class PermissionDef(models.Model):
"""
权限定义表(开发者维护,系统内置,不随租户变化)
此表在 django-tenants shared schema 中,所有租户共用。
"""
code = models.CharField(max_length=100, unique=True) # 如 "client.view_private_scope"
module = models.CharField(max_length=50, choices=PermissionModule.choices)
sub_module = models.CharField(max_length=50, blank=True) # 如 "二手&租赁"
group_name = models.CharField(max_length=100) # 分组标题,如 "私客基础权限"
name = models.CharField(max_length=200) # 显示名称
description = models.TextField(blank=True)
value_type = models.CharField(max_length=20, choices=ValueType.choices)
# 范围型权限的可选枚举值JSON 存储 ScopeLevel 的 value 列表)
# 例:[1, 2, 3, 5] 表示只提供「本人/本组/本门店/全公司」四个选项
scope_choices = models.JSONField(default=list, blank=True)
# 系统最小默认值
default_value = models.JSONField(default=dict) # {"v": false} / {"v": 0} / {"v": 0}
sort_order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["module", "sub_module", "sort_order"]
def __str__(self):
return f"[{self.module}] {self.name}"
class RoleCategory(models.TextChoices):
AGENT = "agent", "置业顾问"
MANAGER = "manager", "店管"
DIRECTOR= "director","总经"
class Role(models.Model):
"""
角色(租户内数据,在 tenant schema 中)
"""
name = models.CharField(max_length=100)
category = models.CharField(max_length=50, choices=RoleCategory.choices)
description = models.TextField(blank=True)
# 引用来源角色(从哪个角色模板复制)
template_role = models.ForeignKey(
"self", null=True, blank=True,
on_delete=models.SET_NULL,
related_name="derived_roles"
)
created_by = models.ForeignKey("org.Staff", on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ("name",) # 租户内角色名唯一
def __str__(self):
return self.name
class RolePermission(models.Model):
"""
角色的权限配置(角色 × 权限项 → 值)
只存储非默认值的项,减少存储量。
"""
role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name="permissions")
permission_def = models.ForeignKey(PermissionDef, on_delete=models.CASCADE)
value = models.JSONField() # {"v": true} | {"v": "store"} | {"v": 50}
class Meta:
unique_together = ("role", "permission_def")
class StaffRole(models.Model):
"""
员工 ↔ 角色(多对多,支持一人多角色)
"""
staff = models.ForeignKey("org.Staff", on_delete=models.CASCADE, related_name="staff_roles")
role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name="staff_roles")
assigned_at = models.DateTimeField(auto_now_add=True)
assigned_by = models.ForeignKey(
"org.Staff", on_delete=models.SET_NULL, null=True,
related_name="role_assignments_made"
)
class Meta:
unique_together = ("staff", "role")
class StaffPermissionOverride(models.Model):
"""
员工个人权限覆盖(稀疏存储,只记录与角色不同的项)
这是「个性化调整」的核心表。
"""
staff = models.ForeignKey("org.Staff", on_delete=models.CASCADE, related_name="permission_overrides")
permission_def = models.ForeignKey(PermissionDef, on_delete=models.CASCADE)
value = models.JSONField()
# 记录是谁修改的,为后续日志奠基
modified_by = models.ForeignKey(
"org.Staff", on_delete=models.SET_NULL, null=True,
related_name="permission_overrides_made"
)
modified_at = models.DateTimeField(auto_now=True)
note = models.TextField(blank=True) # 管理员备注
class Meta:
unique_together = ("staff", "permission_def")
```
---
## 四、权限解析引擎
```python
# apps/permissions/services/resolver.py
import json
import redis
from django.conf import settings
from .models import PermissionDef, RolePermission, StaffPermissionOverride, ValueType, ScopeLevel
_redis = redis.Redis.from_url(settings.REDIS_URL)
CACHE_TTL = 3600 # 1小时变更时主动失效
def _merge_values(value_type: str, values: list) -> dict:
"""
多角色权限值合并:最宽松原则
values: 每个角色的原始值字典列表,如 [{"v": true}, {"v": false}]
"""
raw = [v["v"] for v in values if v]
if not raw:
return None
if value_type == ValueType.BOOLEAN:
return {"v": any(raw)} # OR
if value_type == ValueType.SCOPE:
# 将 scope 字符串转为整数比较,取最大
scope_map = {s.label: s.value for s in ScopeLevel}
int_vals = [scope_map.get(r, 0) for r in raw]
best = max(int_vals)
# 转回字符串
label_map = {s.value: s.name.lower() for s in ScopeLevel}
return {"v": label_map.get(best, "none")}
if value_type == ValueType.INTEGER:
# 0 = 不限制(最宽松),否则取最大值
if 0 in raw:
return {"v": 0}
return {"v": max(raw)}
return values[0]
def get_resolved_permissions(staff_id: int, tenant_schema: str) -> dict:
"""
获取员工完整权限快照(含缓存)
返回格式: {"permission_code": {"v": value}, ...}
"""
cache_key = f"perm:{tenant_schema}:{staff_id}"
cached = _redis.get(cache_key)
if cached:
return json.loads(cached)
snapshot = _build_permission_snapshot(staff_id)
_redis.setex(cache_key, CACHE_TTL, json.dumps(snapshot))
return snapshot
def _build_permission_snapshot(staff_id: int) -> dict:
"""构建员工权限快照(不含缓存逻辑)"""
from apps.permissions.models import StaffRole
# Step 1: 获取员工所有角色
role_ids = list(
StaffRole.objects.filter(staff_id=staff_id).values_list("role_id", flat=True)
)
# Step 2: 拉取所有权限定义
all_defs = {p.code: p for p in PermissionDef.objects.filter(is_active=True)}
# Step 3: 从角色权限表聚合,按 permission_def 分组
role_perms_qs = RolePermission.objects.filter(role_id__in=role_ids).select_related("permission_def")
role_values_by_code: dict[str, list] = {}
for rp in role_perms_qs:
code = rp.permission_def.code
role_values_by_code.setdefault(code, []).append(rp.value)
# Step 4: 合并多角色值
merged: dict[str, dict] = {}
for code, values in role_values_by_code.items():
pdef = all_defs.get(code)
if pdef:
merged[code] = _merge_values(pdef.value_type, values)
# Step 5: 填入未配置的权限默认值
for code, pdef in all_defs.items():
if code not in merged:
merged[code] = pdef.default_value
# Step 6: 叠加个人覆盖(最高优先级)
overrides = StaffPermissionOverride.objects.filter(staff_id=staff_id).select_related("permission_def")
for override in overrides:
merged[override.permission_def.code] = override.value
return merged
def invalidate_staff_cache(staff_id: int, tenant_schema: str):
"""权限变更后调用此方法清除缓存"""
cache_key = f"perm:{tenant_schema}:{staff_id}"
_redis.delete(cache_key)
def invalidate_role_cache(role_id: int, tenant_schema: str):
"""角色权限变更后,清除所有使用该角色的员工缓存"""
from apps.permissions.models import StaffRole
staff_ids = StaffRole.objects.filter(role_id=role_id).values_list("staff_id", flat=True)
keys = [f"perm:{tenant_schema}:{sid}" for sid in staff_ids]
if keys:
_redis.delete(*keys)
```
---
## 五、权限检查工具Views 层集成)
```python
# apps/permissions/checks.py
from functools import wraps
from django.http import HttpResponseForbidden
from .services.resolver import get_resolved_permissions
from .models import ScopeLevel
class PermissionChecker:
"""
在 View 或模板中使用的权限检查器
用法:
checker = PermissionChecker(request.staff, request.tenant.schema_name)
if checker.can("client.view_private_scope", min_scope="store"):
...
"""
def __init__(self, staff, tenant_schema: str):
self._staff = staff
self._perms = get_resolved_permissions(staff.id, tenant_schema)
def get(self, code: str):
"""获取权限原始值"""
entry = self._perms.get(code, {})
return entry.get("v")
def is_enabled(self, code: str) -> bool:
"""布尔型:是否开启"""
return bool(self.get(code))
def has_scope(self, code: str, min_scope: str) -> bool:
"""
范围型:是否达到最低所需范围
min_scope: "self" | "group" | "store" | "region" | "company"
"""
scope_value = self.get(code)
if scope_value is None:
return False
scope_map = {s.name.lower(): s.value for s in ScopeLevel}
actual = scope_map.get(scope_value, 0)
required = scope_map.get(min_scope, 0)
return actual >= required
def get_limit(self, code: str) -> int:
"""数值型获取上限0=不限制)"""
v = self.get(code)
return v if isinstance(v, int) else 0
def is_unlimited(self, code: str) -> bool:
"""数值型:是否不限制"""
return self.get_limit(code) == 0
def permission_required(code: str, value_check=None):
"""
View 装饰器:检查权限
value_check: 可选的 lambda接收权限值返回 bool
示例:
@permission_required("client.view_private_scope",
lambda v: ScopeLevel[v.upper()].value >= ScopeLevel.STORE.value)
def my_view(request): ...
"""
def decorator(view_func):
@wraps(view_func)
def wrapped(request, *args, **kwargs):
checker = PermissionChecker(request.staff, request.tenant.schema_name)
val = checker.get(code)
if value_check:
ok = value_check(val)
else:
ok = bool(val)
if not ok:
return HttpResponseForbidden("权限不足")
return view_func(request, *args, **kwargs)
return wrapped
return decorator
```
---
## 六、Django Admin / View 层权限编辑
### 保存角色权限HTMX 请求)
```python
# apps/permissions/views.py
from django.views import View
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from .models import Role, RolePermission, PermissionDef
from .services.resolver import invalidate_role_cache
class RolePermissionSaveView(View):
"""
接收角色权限编辑页的 HTMX 保存请求
POST /permissions/roles/<role_id>/save/
body: {"perms": {"client.view_private_scope": {"v": "store"}, ...}}
"""
def post(self, request, role_id):
role = get_object_or_404(Role, pk=role_id)
data = json.loads(request.body)
perm_data = data.get("perms", {})
for code, value in perm_data.items():
pdef = PermissionDef.objects.get(code=code)
RolePermission.objects.update_or_create(
role=role,
permission_def=pdef,
defaults={"value": value}
)
# 清除所有使用该角色的员工缓存
invalidate_role_cache(role.id, request.tenant.schema_name)
# 返回 HTMX 片段Toast 提示
return HttpResponse(
'<div x-data x-init="$dispatch(\'toast\', {msg: \'保存成功\', type: \'success\'})" />'
)
```
### 保存个人权限覆盖
```python
class StaffPermissionOverrideSaveView(View):
"""
保存员工个人权限覆盖
POST /permissions/staff/<staff_id>/override/
"""
def post(self, request, staff_id):
from apps.org.models import Staff
from .models import StaffPermissionOverride
from .services.resolver import invalidate_staff_cache
staff = get_object_or_404(Staff, pk=staff_id)
data = json.loads(request.body)
for code, value in data.get("overrides", {}).items():
pdef = PermissionDef.objects.get(code=code)
StaffPermissionOverride.objects.update_or_create(
staff=staff,
permission_def=pdef,
defaults={"value": value, "modified_by": request.staff}
)
invalidate_staff_cache(staff.id, request.tenant.schema_name)
return HttpResponse('<div x-data x-init="$dispatch(\'toast\', {msg: \'个人权限已更新\'})" />')
```
---
## 七、「权限与角色权限不一致」标记逻辑
```python
# apps/permissions/services/consistency.py
def get_staff_inconsistent_permission_codes(staff_id: int, tenant_schema: str) -> list[str]:
"""
返回该员工中与其角色默认权限不一致的 permission code 列表
用于在人员列表中标记橙色「不一致」状态
"""
from apps.permissions.models import StaffPermissionOverride, StaffRole, RolePermission
from .resolver import _merge_values, _build_permission_snapshot
# 只看个人覆盖了哪些
overrides = dict(
StaffPermissionOverride.objects.filter(staff_id=staff_id)
.values_list("permission_def__code", "value")
)
if not overrides:
return []
# 构建纯角色合并结果(不含个人覆盖)
role_ids = list(StaffRole.objects.filter(staff_id=staff_id).values_list("role_id", flat=True))
role_perms_qs = RolePermission.objects.filter(role_id__in=role_ids).select_related("permission_def")
role_values_by_code: dict[str, list] = {}
for rp in role_perms_qs:
code = rp.permission_def.code
role_values_by_code.setdefault(code, []).append(rp.value)
inconsistent = []
for code, override_val in overrides.items():
pdef = PermissionDef.objects.filter(code=code).first()
if not pdef:
continue
role_merged = _merge_values(pdef.value_type, role_values_by_code.get(code, []))
if role_merged is None:
role_merged = pdef.default_value
if override_val != role_merged:
inconsistent.append(code)
return inconsistent
def staff_has_inconsistency(staff_id: int, tenant_schema: str) -> bool:
"""快捷方法:员工是否存在个人权限不一致"""
return len(get_staff_inconsistent_permission_codes(staff_id, tenant_schema)) > 0
```
---
## 八、前端集成HTMX + Alpine.js
### 权限编辑页骨架(按模块懒加载)
```html
<!-- templates/permissions/staff_permission_edit.html -->
<div x-data="{ activeModule: 'client' }">
<!-- 左侧模块导航 -->
<nav>
{% for module in modules %}
<button
@click="activeModule = '{{ module.code }}'"
:class="activeModule === '{{ module.code }}' ? 'active' : ''"
hx-get="/permissions/staff/{{ staff.id }}/module/{{ module.code }}/"
hx-target="#perm-panel"
hx-trigger="click"
hx-swap="innerHTML">
{{ module.name }}
</button>
{% endfor %}
</nav>
<!-- 右侧权限配置面板HTMX 懒加载,避免一次性渲染数百条) -->
<div id="perm-panel"
hx-get="/permissions/staff/{{ staff.id }}/module/client/"
hx-trigger="load">
<div class="loading-spinner">加载中...</div>
</div>
</div>
```
### 权限项组件(范围型下拉)
```html
<!-- templates/permissions/partials/perm_scope_item.html -->
<div x-data="{ editing: false, value: '{{ current_value }}' }" class="perm-item">
<span class="perm-name">{{ pdef.name }}</span>
<span class="perm-desc text-muted">{{ pdef.description }}</span>
<!-- 范围型下拉 -->
<select
x-model="value"
hx-post="/permissions/staff/{{ staff.id }}/override/"
hx-vals='js:{"overrides": {"{{ pdef.code }}": {"v": $el.value}}}'
hx-trigger="change"
hx-swap="none">
{% for choice in pdef.scope_choices_display %}
<option value="{{ choice.value }}" {% if choice.value == current_value %}selected{% endif %}>
{{ choice.label }}
</option>
{% endfor %}
</select>
<!-- 编辑按钮打开 Drawer -->
<button
hx-get="/permissions/pdef/{{ pdef.id }}/drawer/?staff_id={{ staff.id }}"
hx-target="#perm-drawer"
hx-trigger="click">
编辑
</button>
</div>
```
---
## 九、目录结构建议
```
apps/permissions/
├── models.py # 所有权限相关 Model见第三节
├── admin.py # Django AdminPermissionDef 管理
├── views.py # Role/Staff 权限保存、模块面板加载
├── urls.py
├── services/
│ ├── resolver.py # 权限解析引擎(含 Redis 缓存)
│ ├── consistency.py # 不一致标记逻辑
│ └── merger.py # 多角色值合并函数(单独抽离方便单测)
├── templatetags/
│ └── permission_tags.py # 模板标签:{% has_perm "client.view_private" %}
├── fixtures/
│ └── permission_defs.json # 初始权限目录数据
└── migrations/
```
---
## 十、关键风险与缓解
|风险|应对方案|
|---|---|
|角色权限变更未实时生效|`invalidate_role_cache()` 在保存时同步调用,清除所有相关员工缓存|
|`PermissionDef` 数据量大300+条)前端渲染慢|左侧模块导航驱动 HTMX 懒加载,每次只渲染当前模块|
|INTEGER 型 `0=不限制` 语义混淆|`is_unlimited()` 工具方法封装判断,避免散落在业务代码中|
|多角色 SCOPE 合并需要枚举顺序一致|`ScopeLevel` 使用 `IntegerChoices`,排序由整数值保证,不依赖字符串比较|
|`django-tenants` Schema 隔离|`PermissionDef` 放入 `shared_apps``Role/RolePermission/StaffRole/Override` 放入 `tenant_apps`|
|角色被删除时员工无角色|删除前校验 `StaffRole.objects.filter(role=role).exists()`,阻止删除并提示迁移|
---
## 十一、迁移执行顺序
1. 创建 `PermissionDef` fixture所有权限定义约 300 条)并执行 `loaddata`
2. 建立 `Role``RolePermission``StaffRole``StaffPermissionOverride`
3.`org.Staff` 增加权限相关的属性方法(`get_permission_checker()`
4. 部署 `CACHE_TTL``invalidate_*` 调用点
5. 实现管理 UI人员列表 → 角色管理 → 个人权限编辑)
---
_文档版本 v1.0 | 生成时间 2026-04-24_