吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 529|回复: 9
上一主题 下一主题
收起左侧

[Python 原创] 基于【Python】的【django】框架搭建的【RBAC】为基础的Superbox起手包

[复制链接]
跳转到指定楼层
楼主
xulei 发表于 2026-9-11 09:29 回帖奖励
本帖最后由 xulei 于 2026-9-11 09:31 编辑

SuperBox 使用手册

(基于【Python】的【django】框架搭建的【RBAC】为基础的起手包)

适用对象:会用 Django 写功能模块、但不想从零搭建 RBAC 的开发者。
本手册回答三个问题:这是什么?怎么跑起来?我怎么在上面开发自己的模块并自动受权限管理?
作者的话:
作为新手,被AI骗过,被django admin骗过。导致搭出来的django没有想象中的rbac。后来知道真相后去开源社区找起手包也不尽人意,无奈只能自己做。
1.属于闭门造车类型,所以若诸君能给点意见,一定虚心接受。
2.如果有好的开源的起手包,劳烦告知一下,跪谢。


第 0 章 30 秒明白这是什么

SuperBox 是一个 RBAC 起手包:拿到的不是"开箱即用的产品",而是一套跑得起来的源码工程——你用 PyCharm 打开、连上自己的数据库,就可以直接开始写自己的功能模块,权限体系自动生效。

先说清它和 Django admin 的区别——这是最容易产生的误会:

  • admin:给数据库表做增删改查的后台,它帮你管理"数据";
  • SuperBox:给你的业务系统前台预装"用户 / 角色 / 权限"这套管理能力,外加一个带侧边栏的工作台骨架。它管的是"你的系统的哪个菜单和按钮可以被哪些用户使用"。

打开包你会看到用户管理、角色管理这些页面,看起来有点像 admin——但目的完全不同:admin 让你管理表记录;SuperBox 让你不用自己写,就拥有一套用户权限体系,而且你后面写的每个页面都能自动挂进这套体系。还有一层差异:admin 的"组权限"绑定的是模型级增删改查(Can add user / Can change group 那种),而业务系统要的是菜单级、按钮级权限——照着 admin 的思路做会万劫不复。

你拿到手有什么:

类别 内容
RBAC 完整功能 用户管理(新增/编辑/删除/重置密码)、角色管理、菜单管理、角色绑定权限(勾选式授权)
权限能力 目录/菜单/按钮三级功能点、按钮级显隐控制、后端拦截、超管全权
前端工作台 登录页 + 顶栏 + 左侧菜单 + 右侧 iframe 工作区(点菜单页内打开,不整页跳转)
主题 蓝 / 暗黑 / 浅色三套主题,iframe 内页面自动跟随
开发环境 Python 3.14.3 + Django 6.1.1,.venv 已打包在内,第三方依赖仅 mysqlclient

版本号说明:本包刻意基于发布时最新的 Python 3.14 + Django 6.1 构建(不带历史包袱)。

一句话定位:跳过"从零搭 RBAC"这一步,直接写业务的起手包。


第 1 章 为什么有这个项目

用 Django 做业务系统,权限管理绕不开。而两条常规路线都不省心:

  1. django.contrib.admin 很误导人。 它就是第 0 章说的那种"管理数据表"的后台,不是业务系统的权限体系——但很多初学者以为把模型注册进 admin 就算做了权限,真到做业务系统时发现完全不是一回事。
  2. 从头搭一套 RBAC 费时费力。 用户、角色、菜单、授权、按钮级控制、侧边栏按权限过滤……每一项都不难,加起来却要搭好几天。

所以有了这个项目:最新版 Python + Django、尽量零第三方插件、绕开 admin 的 RBAC 起手包。你拿到源码,专注写自己的业务模块即可。

适合谁: 会写 Django、想快速起业务系统的开发者。

不适合谁: 需要开箱即用的数据级权限、工作流、审计日志的团队——本包只管功能权限(谁能用哪个功能)。数据级权限(比如"只能看自己的数据")并不难做,第 5 章的 demo 会现场演示一遍。


第 2 章 从拿到源码到跑起来

2.1 前置条件

  • PyCharm:社区版(免费)即可(作者用的是专业版,激活方式网上一堆)——打开本项目、在 Terminal 里执行手册中的所有命令,完全够用。社区版没有 Django 专属便利(新建 Django 项目向导、Django 模板语法支持、manage.py 图形化运行配置等),这些是专业版才有的;用社区版跟着本手册操作没有障碍,但是最好用专业版。
  • MySQL 已安装并在本机运行(127.0.0.1:3306;8.4 及以上均可。本包已内置 MySQL 驱动 mysqlclient,无需另装)。

2.2 命令在哪执行

所有 python manage.py … 命令都要在项目自带的 .venv 环境里执行(系统 Python 没装 Django,直接跑会报 No module named django),并且都在项目根目录manage.py 所在的目录)执行。

推荐方式:用 PyCharm 的终端。 打开项目后,点窗口底部的 Terminal(找不到就走菜单 View → Tool Windows → Terminal)。PyCharm 会自动激活项目里的 .venv——终端提示符前面出现 (.venv),就说明环境对了,之后所有命令都在这里敲。

也可以用系统终端手动激活(这是 AI 写的,我没试过):

# Windows
.venv\Scripts\activate

# macOS / Linux
source .venv/bin/activate

如果 .venv 激活失败(换机器常见——它内部记录的是打包电脑的 Python 路径),把 .venv 文件夹删掉重建即可(需先从 python.org 安装 Python 3.14):

python -m venv .venv
# Windows 用 .venv\Scripts\activate,macOS/Linux 用 source .venv/bin/activate
pip install django==6.1.1 mysqlclient

重建后一切照旧。另外:Windows 上 mysqlclient 一般直接装得上;若安装时报编译错误,请自己网上查,我也不知道。

2.3 八步跑起来

以下步骤默认你已经在 (.venv) 环境中、项目根目录下。

第 1 步 · 解压源码包,用 PyCharm 打开项目根目录。

第 2 步 · 确认解释器。 File → Settings → Project → Python Interpreter,指向项目内的 .venv(已随包打好,无需重建环境)。

第 3 步 · 建数据库。 在 MySQL 里执行:

CREATE DATABASE superbox DEFAULT CHARACTER SET utf8mb4;

实际上我用的是 Navicat 创建,字符集选 utf8mb4,排序规则选 utf8mb4_unicode_ci。

第 4 步 · 改数据库密码。 打开 superbox_py/settings.py,把 DATABASES 里的 MySQL 账号密码改成你自己的(库名 superbox,地址默认 127.0.0.1:3306)。

认准位置:要改的是 manage.py 同级的 superbox_py 文件夹里的那个 settings.py。如果你的解压文件夹恰好也叫 superbox_py,完整路径就是 superbox_py/superbox_py/settings.py——外层是工程根目录,内层才是 Django 配置包(8.1 有完整文件地图)。

第 5 步 · 建表。

python manage.py migrate

看到终端逐行输出 Applying superbox.0001_initial... OK 之类的迁移记录(每条后面带 OK),这一步就成了。

第 6 步 · 建超管账号。

python manage.py createsuperuser

终端会依次让你输入 Username(用户名)、Email address(邮箱,可直接回车跳过)、Password(密码,输入时屏幕不显示是正常的),最后提示 Superuser created successfully.。这个账号就是超级管理员,自动拥有全部权限。

第 7 步 · 同步菜单。

python manage.py sync_menus

输出 同步完成,共处理 14 个菜单/按钮(数字以实际输出为准,重点是出现"同步完成"字样)。

⚠️ 这一步别跳过。 sync_menus 必须在第一次登录之前跑过一次——菜单表还空着就登录,侧边栏会什么都看不到。这不算坏,只是菜单还没进库。

第 8 步 · 启动。

python manage.py runserver

输出 Starting development server at http://127.0.0.1:8000/。浏览器打开 http://127.0.0.1:8000/superbox/,用第 6 步建的账号登录。


第 3 章 先把系统用起来

跑起来了,别急着写代码——先花十分钟把系统"用"一遍。走完这一遍你就明白这套 RBAC 到底在管什么;而且你自己开发完模块之后,配权限靠的就是这套动作。

侧边栏「系统管理」下面有 4 个页面,这一章只用到其中两个:角色管理(建角色)和权限绑定(给角色勾权限)。另外两个(用户管理、菜单管理)你随手点点就懂了。

先看一眼整趟旅程——你现在正走"使用篇"这一段,走完这一章就通关了:

flowchart LR
    subgraph SG1[使用篇]
    A[拿到源码包] --> B[PyCharm 打开<br>跑起来] --> C[认识系统] --> D[建角色<br>绑权限] --> E[建用户<br>加入角色] --> F[普通用户登录<br>验证权限效果]
    end
    subgraph SG2[开发篇]
    G[写声明开发模块<br>第 4、5 章] --> H[sync_menus<br>给角色授权] --> I[完成]
    end
    F --> G

3.1 建一个角色

「角色管理」→ 新增 → 名称填"业务员" → 保存。

你该看到:回到列表页,"业务员"出现在角色列表里。

3.2 给这个角色勾上权限

「权限绑定」→ 左侧选中"业务员" → 右侧勾选:勾「用户管理」菜单,勾「新增用户」「编辑用户」,不勾「删除用户」;再勾上「保存绑定」按钮本身 → 点保存。

你该看到:保存成功的提示。

(连"保存"这个按钮都单独管控,正是按钮级权限的体现——不勾它,这个角色进了绑定页也保存不了。)

3.3 建个用户,把它加进这个角色

「用户管理」→ 新增 → 用户名 xiaowang、设个密码 → 角色选"业务员" → 保存。

你该看到:用户列表里出现 xiaowang,角色一栏写着"业务员"。

3.4 换个账号登录,看看区别

退出登录,用 xiaowang 登录。对比超管,你该看到三件事:

  • 侧边栏还是「系统管理」分组,但里面只剩"用户管理"一个页面——没勾的页面根本不出现;
  • 进用户管理:新增、编辑按钮都在,"删除"按钮不存在——不是置灰,是压根没渲染;
  • 在地址栏直接输一条删除地址(如 /superbox/users/1/delete/,把 1 换成列表里任一用户的编号)→ 得到 403 页面——绕过界面也拦得住。

再切回超管对比:什么都看得到、什么都能点。这就是整套系统的意义——同一个系统,不同的人看到不同的功能,界面和后端两层都拦得住。

3.5 小结

到这里你已经会"用"这套系统了:建角色 → 绑权限 → 建用户 → 生效。以后给新同事开账号,就是这套动作。

从下一章开始我们反过来:不再用鼠标点,而是在代码里写声明,让你自己开发的模块也接进这套权限体系。


第 4 章 亲手加个功能,顺便搞懂 RBAC

本章你只要记住一件事:开发新功能模块,就是在视图函数头上写几个装饰器,新模块就自动被 RBAC 管起来。要用到的东西一共 4 个——@menu(声明功能点)、sync_menus(同步进库)、@require_perm(拦截无权限)、user_perm_codes(控制按钮显隐)。

动手前先对齐三个词,就三行,后面不再重复解释:

  • 用户 = Django 自带的 auth.User,就是登录账号;
  • 角色 = Django 自带的 auth.Group。本包没有新建角色模型,所以你在代码里搜不到 "Role" 字样——看到的 Group 就是角色
  • 功能点 = 本包的 Menu 模型,一条记录 = 一个可以授权的东西(分目录 / 菜单 / 按钮三种,往下马上就用上)。

关系只有一句话:用户属于角色,角色绑定功能点。给谁开权限,就是把用户加进角色。

不堆概念了,直接跟着做一遍——做完你自然就懂了。

4.1 跟我做一遍:从"我想要一个分组"到"按钮都能用"

这一节我们不罗列知识点,跟着一个真实需求走一遍。

需求是这样的——做一个"待办清单"

  1. 侧边栏有个「我的工作台」分组;
  2. 分组里有个"待办清单"页面,打开能看到自己的待办;
  3. 页面上能添加新的待办;
  4. 每条待办能标记完成、能删除。

我们从第 1 条开始,一路做到第 3 条(第 4 条在下一章补齐)。中间每冒出一个新东西,都先说清它是什么,再写代码。

之所以选待办清单:它足够小,但把"一个模块该有的东西"全占了——分组、页面、按钮、数据隔离。你做完这一个,再想做别的模块就是换个名字的事。

第一步:先建个"分组",这种东西我们叫它 dir

这是什么:你想要侧边栏上有个能展开收起的分组标题(就像"系统管理"那样)。这种东西在本包里叫 dir(目录,也就是侧边栏的分组)。它只有一个特点要记住:它只是个标题,没有自己的页面——点它不会跳到任何地方,它下面挂的页面才是能点的。

怎么写:因为它没有页面,就没有函数可以给它装饰,所以 dir 是单独写一行(注意前面没有 @):

menu(name="我的工作台", code="todo", type="dir", order=2)

四个参数各是什么意思(一个一个说):

  • name="我的工作台"——侧边栏上显示的文字。随便改,改了不影响任何权限,只是换个叫法;
  • code="todo"——它的身份证号,全系统唯一。★ 这个定下来就别改;
  • type="dir"——告诉系统"我是个分组,不是页面";
  • order=2——排序号。包内"系统管理"是 1,给 2 就排在它后面。

不写 / 写错会怎样

  • type="dir" 不写 → 默认是 "menu",系统以为它是个页面,会去找它的 url(而你没给),侧边栏就显示成一个点不动的死条目;
  • 前面加了 @、写成 @menu(...) 单独一行 → Python 语法错误,装饰器必须紧跟在函数定义上面。

现在跑一下,看看效果。 在终端执行:

python manage.py sync_menus

然后刷新浏览器——侧边栏会多出一个「我的工作台」分组,但它是空的,展开什么都没有。这很正常,因为分组里还没放页面。

这个动作要记住:以后每次改了 @menu 声明,都要跑一次 sync_menus。你的声明写在代码里,但侧边栏读的是数据库sync_menus 就是把声明从代码搬进数据库的那一步。不跑它,你改的代码等于没生效,导航栏不会有任何变化。

第二步:给这个分组加个能点开的页面(menu)

现在的需求:分组是空的,我想让它下面有个能点开的页面"待办清单"。

这是什么:这种"有页面、点得开"的节点叫 menu(菜单)。它和 dir 的区别就一条——它有 url,点了会跳到具体页面。

怎么写

@menu(name="待办清单", code="todo:list", parent="todo", type="menu", url="/superbox/todos/", order=1)
@login_required
@require_perm("todo:list")
def todo_list(request):
    return render(request, "superbox/todo_list.html")

先说 parent——这是全篇最关键的一根线

  • parent="todo" 就是把"待办清单"挂到"我的工作台"下面的那根线
  • ★ 这里填的是父节点的 code 字符串"todo")——不是显示名"我的工作台",更不是数据库里的 id 数字;
  • 写错会怎样:如果写成一个根本不存在的码(比如手滑写成 parent="todo:xxx"),这根线就断了。sync_menus 找不到爸爸,只能把"待办清单"扔到最外层,变成和"系统管理"平级的孤儿——你在侧边栏会看到它孤零零地挂在外面,并不在"我的工作台"里面。

再说 url

  • url="/superbox/todos/" 是点这个菜单时右侧打开的地址;
  • ★ 必须和你 urls.py 里配的地址一字不差,包括 /superbox/ 前缀。少了前缀的话,菜单照常显示,点了就 404。

最后看下面那两个装饰器各管什么(这段是安全的关键,得说透):

  • @login_required——没登录的人访问 → 送回登录页;
  • @require_perm("todo:list")——登录了但没这个权限的人 → 403 无权限页。
    • ★ 括号里的 "todo:list" 必须和上面 @menu 的 code 一模一样。写错了(比如写成 "todo")就永远 403,而且超管还看不出这个问题——has_perm_code 里超管直接放行,只有普通用户才暴露;
    • 这一行才是真正拦住"手输网址"的防线。侧边栏只是让你"看不见",拦不住知道地址的人;@require_perm 不管你是点菜单进来的、手输地址栏、还是用工具构造请求,都得过这一关。

光写视图还不够,还得补两件事

  1. superbox/urls.py 里加一条路由:path("todos/", views.todo_list, name="todo_list")——地址要和上面的 url= 对上;
  2. 新建 templates/superbox/todo_list.html,最小内容一行就够:<h1>待办清单</h1>

现在同步一下,刷新看看:跑 python manage.py sync_menus,再刷新浏览器——「我的工作台」下面出现了"待办清单"。点它,右侧能看到你的页面。

如果点开是 404 或 TemplateDoesNotExist,回头检查上面那两件事漏了哪件(路由 / 模板)。

第三步:突发奇想,想在页面上加个按钮

现在的需求:列表页能看了,我想在上面放个"新增待办"按钮。

这是什么:页面上做某个操作的(新增 / 编辑 / 删除)叫 button(按钮)。它和 menu 最大的区别是没有自己的页面——所以不填 url

怎么写

@menu(name="新增待办", code="todo:list:add", parent="todo:list", type="button")
@require_POST
@login_required
@require_perm("todo:list:add")
def todo_create(request):
    ...

... 是占位符,Python 里合法,表示"函数体先空着";完整写法见第 5 章。)

parent 挂给谁——这里最容易错

  • parent="todo:list"——挂在菜单(页面)下面,不是目录 todo
  • 为什么:按钮是"某个页面里的操作",它属于那个页面。挂到 todo 上,按钮就跑到不该在的位置去了。

code 怎么起名字

  • code="todo:list:add" 用冒号分层,意思就是"这是谁的动作":todo:list 是页面,add 是这个页面上的新增;
  • 记一句就够:parent 就是 code 去掉最后一段——todo:list:add 的爸爸是 todo:listtodo:list 的爸爸是 todo

@require_POST 是干什么的,不写会怎样

  • 意思是"只接受表单提交(POST)";
  • 不写会怎样:别人发你一个链接(GET 请求),你手一抖点开就把数据改了或删了;而且浏览器会偷偷预取页面上的链接,你没点也可能触发。

★★★ 每个按钮都要单独声明,一个都不能漏

  • 漏了某个按钮的 @menu,这个权限点就不会进数据库
  • 后果:角色绑定页上根本没这个东西可勾 → 按钮永远不显示 → 就算手工构造请求也会被 403 拦掉。

模板里怎么让它显示(按钮级权限就是这么落地的):

{% if "todo:list:add" in user_perm_codes %}
<a class="btn primary" href="...">+ 新增待办</a>
{% endif %}

被授权的角色能看到它,没授权的连这个按钮都不渲染——不是置灰,是压根不存在。

现在同步一下,再去授权:跑 python manage.py sync_menus,这样"新增待办"这个权限点才进库,才能在权限绑定页上勾到它。然后去「权限绑定」把它勾给某个角色,用那个角色的用户重新登录——按钮就出现了。

这一步其实和第 3 章做过的动作一模一样,只不过第 3 章勾的是包里自带的权限,这次勾的是你自己刚声明的权限。

第四步:怎么确认权限真的生效了(别用超管自欺)

先准备一个普通用户来测:超管(is_superuser)会自动通过所有权限检查,用超管测等于没测。建角色、勾权限、建用户、登录——就是 3.2 节做过的那套动作。

场景 你应该看到
未登录,直接访问 /superbox/todos/ 跳到登录页
普通用户 A(勾 todo:list)直接输这个网址 403 无权限页
普通用户 A 看侧边栏 压根没有"待办清单"这一项
普通用户 B(勾了 todo:list)直接输网址 正常打开
普通用户 B 但没勾 todo:list:add 页面能看,但"新增待办"按钮不存在

"看不见"(侧边栏、按钮不渲染)和"进不去"(403)是两件事,两层都要验

回头看:刚才这一路用到的三种节点
节点 是什么 有没有页面 parent 挂给谁 url 授权时要勾吗
dir 目录 侧边栏的分组标题 没有 上级目录(可省略) 不填 不用勾(不参与鉴权)
menu 菜单 一个能点开的页面 所属目录的 code 必填 要勾
button 按钮 页面上的一个操作 没有 所属菜单的 code 不填 要勾

顺带说一句层级的边界:数据上 Menu.parent 是自关联外键,理论可以无限层,但侧边栏视觉上只渲染两层base.htmlsubmenu 只嵌套一层),所以推荐结构就是刚做的这三层封顶:dir → menu → button。在 dir 底下再挂一个 dir,第二层会显示成一个点不动的条目。

参数速查表(三种节点通用)
参数 填什么 示例
name 界面上显示的文字 "待办清单"
code 权限编码,全局唯一,定好别改 todo:list
parent 父级的 code 字符串(不是 name,不是数据库 id) todo
type dir / menu / button menu
url 页面地址(只有 menu 需要,含 /superbox/ 前缀) /superbox/todos/
icon 图标,可选,一般留空
order 同层排序,越小越靠前 1
怎么用好:五条
  • code 用冒号分层(todo → todo:list → todo:list:add),定下来就别改——改 code 等于删旧建新,之前角色的绑定全丢;
  • 目录只做分组,不参与鉴权,授权时不用勾它;
  • 按钮挂菜单(parent 写菜单的 code),不挂目录;改状态的操作一律带 @require_POST
  • 改完声明跑 sync_menus,改完授权重新登录;
  • 验证权限效果用普通用户,超管什么都看得到,测不出问题。

这一节是最小骨架:一个目录、一个页面、一个按钮。第 5 章的待办事项模块是同一套写法的完整版(模型 + 视图 + 路由 + 模板 + 测试),那时候你会发现自己已经在照着这套动作做了。

4.2 回头看:补两条容易漏的(可选读)

刚才每一步你都跑了 sync_menus、也看到了侧边栏的变化,所以这一节不再重复流程,只补两件容易忽略的事。

① 权限在三个地方同时生效(刚才你每处都碰到了,这里集中说清它们的分工):

位置 表现 拦的是谁
侧边栏 只列出你有权限的菜单 让你"看不见"
按钮 没权限的按钮根本不渲染 让你"点不到"(注意不是置灰)
视图 @require_perm 抛 403 真正的安全边界——绕过界面直接输网址也拦得住

前两层是"体验"(省得你看到用不了的东西),第三层才是"安全"。所以 @menu@require_perm配对出现——只声明不拦截,等于门开着。

② 包里自带的所有功能点(也是角色绑定页上能勾到的全部东西):

code 层级 用途
system dir 侧边栏「系统管理」分组(不参与鉴权,无需勾选)
system:user menu 用户管理页
system:user:add button 新增用户
system:user:edit button 编辑用户
system:user:del button 删除用户
system:user:reset_pwd button 重置密码(挂在 system:user:edit 之下,是"按钮下再挂按钮"的实例)
system:role menu 角色管理页
system:role:add / :edit / :del button 角色的 新增 / 编辑 / 删除
system:menu menu 菜单管理页
system:perm menu 权限绑定页
system:perm:save button 保存绑定(能进绑定页 ≠ 能保存,按钮级二次校验)

你刚做的「我的工作台」和它的页面、按钮,也会出现在这张清单里——那是你自己在代码里声明的,不在上表之内。


第 5 章 做一个真能干活的模块:待办清单

上一节 4.1 你搭出了"三层骨架"——一个分组、一个页面、一个按钮,而且跑通了。但那个页面是空的,没有一个真正能用的功能。

这一节我们把骨架填满,做一个待办清单:能添加、能勾完成、能删除,数据存进数据库,而且每个人只能看到自己的。做完之后你就掌握了在 SuperBox 上开发功能的完整套路——再想做别的模块,无非是换个字段、换个名字。

本章代码完整可运行,照着敲或复制都行。本包自带的功能里没有待办模块,这些代码是你自己加的;哪天不想要了,把追加的段落删掉就回退了。

先看一眼全程:要动哪些地方

先说清楚终点在哪,免得中途迷路。整个模块一共动 4 个文件、跑 4 条命令,首次跟做大概 30-60 分钟:

步骤 文件 动作
5.1 superbox/models.py 文件末尾追加 Todo 模型(约 15 行)
5.2 superbox/views.py 顶部补 2 个 import;末尾追加 5 条菜单声明 + 4 个视图(约 75 行)
5.3 superbox/urls.py urlpatterns 里追加 4 条路由
5.4 templates/superbox/todo_list.html 新建文件(全文复制)
5.5 superbox/views.py + templates/superbox/welcome.html 替换 welcome 视图 + 统计区追加三张卡(可选)
5.6 终端 makemigrations → migrate → sync_menus → runserver
5.7 superbox/tests.py 追加测试类(可选但推荐)

除 5.4 是新建文件外,其余都是往现有文件里追加,不影响包内已有功能;哪天不想要这个模块了,把追加的段落删掉即可回退。

5.0 先别写代码:花五分钟想清楚三件事

写代码最容易犯的错,是上来就建表、写视图。我们换个顺序——先回答三个问题:做什么、有什么、谁能用。这三件事想清楚了,后面的代码基本是顺理成章。

① 做什么——这个模块有哪些功能

  • 查看待办列表
  • 新增待办
  • 标记完成 / 取消完成
  • 删除待办

② 有什么——每条待办要存哪些东西

字段 含义
title 标题(必填)
is_done 是否完成(默认否)
created_at 创建时间
completed_at 完成时间(标记完成时写入,取消完成时清空)
owner 所属用户(外键)

③ 谁能用(权限设计)——五条功能点

功能 RBAC 层级 code 管什么
(分组) dir 目录 todo 侧边栏分组标题「我的工作台」
查看待办列表 menu 菜单 todo:list 侧边栏显示与否、能不能进这个页面
新增待办 button 按钮 todo:list:add 「添加」按钮显不显示、点了有没有权限
标记完成/取消完成 button 按钮 todo:list:done 「完成」按钮显不显示、点了有没有权限
删除待办 button 按钮 todo:list:del 「删除」按钮显不显示、点了有没有权限

两个设计点,想通了再往下走:

设计点一:按钮 code 带层级前缀。 注意按钮 code 是 todo:list:add 而不是 todo:add——按钮挂在自己的菜单(todo:list)之下,形成 目录.菜单.按钮 的层级。将来要做权限降维(比如"普通员工只给 todo:list,不给删除")时,勾选界面上的结构一目了然:取消勾一个按钮,它下面的子权限也一起收走。parent 就是 code 去掉最后一段——system:user:add 的父级是 system:usersystem:user 的父级是 system。按这个规则命名,父子关系永远不会乱。

设计点二:RBAC 管功能权限,数据权限靠 owner。 "我的待办只能我自己看"不是权限问题——todo:list 给了就是能看待办列表;"只看自己的"是数据隔离问题,靠 owner 字段 + 查询过滤实现(见 5.2)。这两层别混在一起想,混了就会做出别扭的权限设计。

5.1 模型

# superbox/models.py —— 文件末尾追加

class Todo(models.Model):
    title = models.CharField(max_length=200, verbose_name="标题")
    is_done = models.BooleanField(default=False, verbose_name="是否完成")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    completed_at = models.DateTimeField(null=True, blank=True, verbose_name="完成时间")
    owner = models.ForeignKey("auth.User", on_delete=models.CASCADE,
                              related_name="todos", verbose_name="所属用户")

    class Meta:
        verbose_name = "待办"
        verbose_name_plural = "待办"
        ordering = ["is_done", "-created_at"]   # 未完成的在前;同状态里新建的在前

    def __str__(self):
        return self.title

两点说明:

  • owner 是数据隔离的根基。 每条待办都记着"这是谁建的",后面所有查询、修改都围着它过滤。
  • completed_atis_done 是手动联动的。 is_done 置 True 时视图里写 completed_at = timezone.now(),取消时置回 None。没有用 auto_now 之类的自动机制,因为"取消完成要清空"这个动作 Django 不会替你做,得自己管。

然后生成并执行迁移:

python manage.py makemigrations
python manage.py migrate

5.2 视图

# superbox/views.py

# —— 顶部 import 区追加 / 修改 ——
from django.utils import timezone          # 追加(views.py 原本没 import 它)
from .models import Menu, Todo             # 原来是 from .models import Menu,加上 Todo

# —— 菜单声明①:我的工作台(目录)——
# 目录没有页面,单独写一行 menu(...) 登记,不装饰任何函数
menu(name="我的工作台", code="todo", type="dir", order=2)   # order=2,排在「系统管理」(order=1) 之后

@menu(name="待办清单", code="todo:list", parent="todo", type="menu", url="/superbox/todos/", order=1)
@login_required
@require_perm("todo:list")
def todo_list(request):
    qs = Todo.objects.filter(owner=request.user)   # 数据权限:只看自己的
    q = request.GET.get("q", "").strip()
    if q:
        qs = qs.filter(title__icontains=q)
    try:
        per_page = int(request.GET.get("per_page", 20))
    except ValueError:
        per_page = 20
    if per_page not in (10, 20, 50, 200):
        per_page = 20
    page_obj = Paginator(qs, per_page).get_page(request.GET.get("page"))
    return render(request, "superbox/todo_list.html", {
        "todos": page_obj, "page_obj": page_obj, "per_page": per_page, "q": q,
    })

# —— 菜单声明:三个按钮,各装饰在对应视图头上 ——
# 按钮视图的 @menu 与包内 user_create / user_edit 同款写法:type="button",parent 指向自己的菜单

@menu(name="新增待办", code="todo:list:add", parent="todo:list", type="button")
@require_POST
@login_required
@require_perm("todo:list:add")
def todo_create(request):
    title = request.POST.get("title", "").strip()
    if not title:
        messages.error(request, "标题不能为空")
    else:
        Todo.objects.create(owner=request.user, title=title)
        messages.success(request, "已添加待办")
    return redirect("superbox:todo_list")

@menu(name="标记完成", code="todo:list:done", parent="todo:list", type="button")
@require_POST
@login_required
@require_perm("todo:list:done")
def todo_toggle_done(request, pk):
    # 双重防越权:@require_perm 管功能权限,owner=request.user 管数据权限
    todo = get_object_or_404(Todo, pk=pk, owner=request.user)
    if todo.is_done:
        todo.is_done = False
        todo.completed_at = None          # 取消完成:清空完成时间
    else:
        todo.is_done = True
        todo.completed_at = timezone.now()  # 标记完成:写入完成时间
    todo.save()
    return redirect("superbox:todo_list")

@menu(name="删除待办", code="todo:list:del", parent="todo:list", type="button")
@require_POST
@login_required
@require_perm("todo:list:del")
def todo_delete(request, pk):
    todo = get_object_or_404(Todo, pk=pk, owner=request.user)
    todo.delete()
    messages.success(request, "已删除")
    return redirect("superbox:todo_list")

先说明上面第一处声明:「我的工作台」那条是单独一行 menu(...)、不装饰任何函数——这是本包的常规写法:目录没有页面,所以没有函数可装饰。

关于 importrender / redirect / get_object_or_404 / messages / login_required / require_POST / Paginator 在 views.py 顶部本来就有,不用重复 import;你只需要补 timezone,并把 .models 的 import 行加上 Todo

逐段讲解:

  • 装饰器顺序是包内铁律,所有视图保持一致:@menu(...) 最上 → @require_POST(如有)→ @login_required@require_perm(...) 最下(紧贴函数)。@menu 不改变函数行为,只负责登记菜单,放最上让它一眼可见。
  • 按钮视图也要声明 @menu,一个都不能漏。 @require_perm 只负责"检查",不负责"登记"——权限点进入菜单表靠的是 @menu 声明 + sync_menus 同步。三个按钮视图如果少了 @menusync_menus 就不会创建 todo:list:add 这些权限点:角色绑定页没得勾、user_perm_codes 里永远不含它们、页面按钮永不渲染且后端永远 403。对照 5.0 的权限设计表:表里 5 条 code,代码里就得有 5 条声明(1 条单独的 dir + 1 条装饰 menu 视图 + 3 条装饰 button 视图)。
  • filter(owner=request.user) 出现两处,一处都不多余。 列表页的过滤防"串数据"——A 用户打开列表页只看到自己的待办;get_object_or_404(Todo, pk=pk, owner=request.user) 防"URL 越权"——A 用户复制一条 B 用户待办的操作链接过来,查询条件里带 owner,查无此条直接 404。RBAC 管的是"你能不能用删除功能",owner 过滤管的是"你能删的是不是自己的",两层缺一不可(呼应 5.0 的设计点二)。
  • toggle / delete 都用 @require_POST 写操作不能做成 GET 链接:现代浏览器会预取页面上的 GET 链接(用户没点也可能触发),而且 GET 请求不带 CSRF 防护。POST 表单 + {% csrf_token %} 才是写操作的正确姿势。
  • messages 的用法messages.success(request, "...") 存一条提示,redirect 之后 frame_base 会自动把提示条渲染在页面顶部,绿成功红失败,一行代码搞定操作反馈。

中途验证点(现在就可以跑): 写到这里不必等全部完工,先跑一遍下面两条命令——如果 5.2 的 import 写漏了(比如 .models 那行忘了加 Todo),或代码里有笔误,它们会立刻报 NameError / ImportError,帮你把问题拦在写页面之前:

python manage.py makemigrations
python manage.py migrate

如果 5.1 已经跑过迁移,这次会输出 No changes detected——同样正常,说明模型层没有遗漏;首次生成时看到 Applying superbox.0003_todo... OK(迁移序号以实际输出为准)即模型就位。接下来可以放心写页面了。

5.3 路由

# superbox/urls.py —— urlpatterns 列表里追加

path("todos/", views.todo_list, name="todo_list"),
path("todos/create/", views.todo_create, name="todo_create"),
path("todos/<int:pk>/toggle/", views.todo_toggle_done, name="todo_toggle_done"),
path("todos/<int:pk>/delete/", views.todo_delete, name="todo_delete"),

四个路由都接在 /superbox/ 前缀下,完整地址即 /superbox/todos/ 等。注意:5.2 里 @menu 声明的 url="/superbox/todos/" 必须和这里第一条路由的实际地址一致(含前缀),否则侧边栏点进去 404。

5.4 页面模板

新建 templates/superbox/todo_list.html,完整内容如下(结构抄自包内 role_list.html,风格完全一致):

模板里的 panel / scroll-area / pager 等 class 都是包内现成的全局样式(定义在 static/superbox/css/app.css),照抄即可,含义和布局原理见第 6 章。

{# templates/superbox/todo_list.html #}
{% extends "superbox/frame_base.html" %}

{% block title %}待办清单 · SuperBox{% endblock %}

{% block content %}
    <div class="panel" style="height:calc(100vh - 28px);">
        <div class="panel-h" style="flex:0 0 auto;">
            <span class="t">待办清单</span>
            <div class="spacer"></div>
        </div>

        {# 顶部工具条:左搜索 + 右新增(新增按钮按权限显隐)#}
        <div style="padding:12px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;flex:0 0 auto;">
            <form method="get" style="display:flex;gap:8px;flex:1;max-width:360px;">
                <input type="text" name="q" value="{{ q }}" placeholder="搜索待办…"
                       style="flex:1;border:1px solid var(--border-strong);border-radius:8px;padding:8px 12px;font-size:13px;background:var(--card-bg);color:var(--text);box-sizing:border-box;">
                <button type="submit" class="btn">搜索</button>
            </form>
            <div class="spacer"></div>
            {% if "todo:list:add" in user_perm_codes %}
            <form method="post" action="{% url 'superbox:todo_create' %}" style="display:flex;gap:8px;">
                {% csrf_token %}
                <input type="text" name="title" maxlength="200" placeholder="要做什么?"
                       required style="width:220px;border:1px solid var(--border-strong);border-radius:8px;padding:8px 12px;font-size:13px;background:var(--card-bg);color:var(--text);box-sizing:border-box;">
                <button type="submit" class="btn primary">+ 添加</button>
            </form>
            {% endif %}
        </div>

        <div class="scroll-area" style="flex:1;min-height:0;">
        <table>
            <thead>
                <tr>
                    <th style="width:60px">完成</th>
                    <th>标题</th>
                    <th style="width:150px">创建时间</th>
                    <th style="width:150px">完成时间</th>
                    <th style="width:80px">操作</th>
                </tr>
            </thead>
            <tbody>
                {% for todo in todos %}
                <tr>
                    <td style="text-align:center;">
                        {# 完成开关也受按钮权限控制:勾选/取消勾选即提交 POST #}
                        {% if "todo:list:done" in user_perm_codes %}
                        <form method="post" action="{% url 'superbox:todo_toggle_done' todo.pk %}" style="display:inline">
                            {% csrf_token %}
                            <input type="checkbox" {% if todo.is_done %}checked{% endif %}
                                   onchange="this.form.submit()"
                                   title="{% if todo.is_done %}点击取消完成{% else %}点击标记完成{% endif %}"
                                   style="cursor:pointer;vertical-align:middle;">
                        </form>
                        {% endif %}
                    </td>
                    <td style="{% if todo.is_done %}text-decoration:line-through;color:var(--text-dim);{% endif %}">{{ todo.title }}</td>
                    <td>{{ todo.created_at|date:"Y-m-d H:i" }}</td>
                    <td>{{ todo.completed_at|date:"Y-m-d H:i"|default:"—" }}</td>
                    <td>
                        {% if "todo:list:del" in user_perm_codes %}
                        <form method="post" action="{% url 'superbox:todo_delete' todo.pk %}" style="display:inline"
                              onsubmit="return confirm('确定删除「{{ todo.title }}」吗?')">
                            {% csrf_token %}
                            <button type="submit" class="op del op-btn">删除</button>
                        </form>
                        {% endif %}
                    </td>
                </tr>
                {% empty %}
                <tr><td colspan="5" class="empty">暂无待办,添加一条开始今天的工作吧</td></tr>
                {% endfor %}
            </tbody>
        </table>
        </div>

        {# 统一分页条:共N条 | ‹ 1/2 › | 每页[10][20][50][200] | 页码跳转 #}
        <div class="pager" style="flex:0 0 auto;">
            <span class="pager-total">共 {{ page_obj.paginator.count }} 条</span>
            {% if page_obj.paginator.num_pages > 1 %}
                {% if page_obj.has_previous %}
                <a class="pager-nav" href="?page={{ page_obj.previous_page_number }}&per_page={{ per_page }}{% if q %}&q={{ q }}{% endif %}" title="上一页" aria-label="上一页">‹</a>
                {% else %}
                <span class="pager-nav disabled" aria-disabled="true" title="已是第一页">‹</span>
                {% endif %}
                <span class="pager-cur">{{ page_obj.number }}<em>/</em>{{ page_obj.paginator.num_pages }}</span>
                {% if page_obj.has_next %}
                <a class="pager-nav" href="?page={{ page_obj.next_page_number }}&per_page={{ per_page }}{% if q %}&q={{ q }}{% endif %}" title="下一页" aria-label="下一页">›</a>
                {% else %}
                <span class="pager-nav disabled" aria-disabled="true" title="已是最后一页">›</span>
                {% endif %}
                <input type="number" id="pageJump" class="pager-jump" value="{{ page_obj.number }}" min="1" max="{{ page_obj.paginator.num_pages }}"
                       title="输入页码后回车跳转"
                       data-base="?per_page={{ per_page }}{% if q %}&q={{ q }}{% endif %}"
                       style="margin-left:6px;">
            {% else %}
                <span class="pager-cur">1<em>/</em>1</span>
            {% endif %}
            <div class="pager-per">
                <span class="label">每页</span>
                <a class="pager-per-opt {% if per_page == 10 %}cur{% endif %}" href="?per_page=10&page=1{% if q %}&q={{ q }}{% endif %}">10</a>
                <a class="pager-per-opt {% if per_page == 20 %}cur{% endif %}" href="?per_page=20&page=1{% if q %}&q={{ q }}{% endif %}">20</a>
                <a class="pager-per-opt {% if per_page == 50 %}cur{% endif %}" href="?per_page=50&page=1{% if q %}&q={{ q }}{% endif %}">50</a>
                <a class="pager-per-opt {% if per_page == 200 %}cur{% endif %}" href="?per_page=200&page=1{% if q %}&q={{ q }}{% endif %}">200</a>
            </div>
        </div>
    </div>

    {# 页码跳转脚本(与 role_list.html 相同)#}
    <script>
    (function(){
        var jump = document.getElementById('pageJump');
        if (!jump) return;
        jump.addEventListener('keydown', function(e){
            if (e.key !== 'Enter') return;
            var p = parseInt(this.value, 10);
            var max = parseInt(this.max, 10);
            if (isNaN(p) || p < 1) p = 1;
            if (p > max) p = max;
            location.href = this.getAttribute('data-base') + '&page=' + p;
        });
    })();
    </script>
{% endblock %}

结构上的几个点(为什么这么写,见第 6 章风格约定):

  • 最外层 panel 高度 calc(100vh - 28px),正好填满工作区;
  • 三段式 flex column:panel-h(标题行)→ 工具条(搜索+新增)→ .scroll-area(表格,flex:1;min-height:0)→ .pager(分页条);
  • 每行的"完成"复选框和"删除"按钮各自包在独立的 POST 表单里,外层按 user_perm_codes 控制渲染;
  • 已完成的行标题加删除线、变暗,视觉状态一目了然。

5.5 首页统计框(可选)

在欢迎页加三张统计卡:全部 / 待完成 / 已完成。定位:welcome 视图就在 superbox/views.py 里(搜索 def welcome),welcome.html 在 templates/superbox/ 下。

# superbox/views.py —— 把 welcome 视图替换为:

@login_required
def welcome(request):
    """欢迎页:工作台默认打开的页面(被 iframe 嵌入的纯内容页)。"""
    my_todos = Todo.objects.filter(owner=request.user)   # 只统计自己的
    return render(request, "superbox/welcome.html", {
        "todo_total": my_todos.count(),
        "todo_open": my_todos.filter(is_done=False).count(),
        "todo_done": my_todos.filter(is_done=True).count(),
    })
{# templates/superbox/welcome.html —— .stat-grid 里追加三张卡(原「当前账号」卡保留)#}
<div class="stat-card">
    <div class="k">我的待办</div>
    <div class="v">{{ todo_total }}</div>
    <div class="d">全部</div>
</div>
<div class="stat-card">
    <div class="k">待完成</div>
    <div class="v">{{ todo_open }}</div>
    <div class="d">还没做的</div>
</div>
<div class="stat-card">
    <div class="k">已完成</div>
    <div class="v">{{ todo_done }}</div>
    <div class="d">今天很棒</div>
</div>

5.6 让模块上线生效:四步

python manage.py makemigrations
python manage.py migrate
python manage.py sync_menus          # ← 新菜单进库
python manage.py runserver

5.2 的中途验证点已经跑过 makemigrations/migrate 的话,这里再跑前两条会显示 No changes detected,属正常,继续往下即可。

用超管账号登录后,你应该看到:

  • 侧边栏出现两个分组:「系统管理」(包内预置,见 4.5)和「我的工作台」(5.2 里那条 type="dir" 声明生成的);
  • 点「我的工作台 → 待办清单」,右侧工作区打开列表页——顶部有搜索框、输入框和「+ 添加」按钮;
  • 在输入框里敲点什么、点「+ 添加」,列表出现一行新待办,页面顶部弹出绿色提示"已添加待办"。

看到以上三样,说明模块已经完整跑通。如果侧边栏没有「我的工作台」,先确认上面的 sync_menus 已经执行(见 2.3 第 7 步)。

然后:登录 → 「系统管理 → 权限绑定」→ 选中角色,把「我的工作台 / 待办清单 / 新增 / 完成 / 删除」全部勾上 → 保存。用户退出重新登录后生效(权限码在登录会话里缓存);或者直接用超管账号看全部。

提醒一点:按钮授权与否不影响功能正确性——本 demo 的使用场景是按钮全给。哪天某个角色只需要"能看不能删",取消勾 todo:list:del 即可:页面上的删除按钮自动消失,后端也同时拦截。这就是"菜单级 + 按钮级权限"的价值:同一套代码,不用改一行就能适配不同角色的权限粒度。

5.7 测试

完整测试示例,覆盖权限与数据隔离两条主线。运行命令:python manage.py test superbox

# superbox/tests.py —— 追加(或直接替换整个文件)

from django.contrib.auth.models import User, Group
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse

from .models import Menu, Todo

class TodoPermissionTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # 测试库是新建的,菜单表是空的——先把代码里的菜单声明同步进去
        call_command("sync_menus")

        # 角色「只读」:只有查看权限;角色「读写」:查看+新增+完成+删除
        role_viewer = Group.objects.create(name="只读")
        role_editor = Group.objects.create(name="读写")
        role_viewer.menu_set.add(Menu.objects.get(code="todo:list"))
        for code in ("todo:list", "todo:list:add", "todo:list:done", "todo:list:del"):
            role_editor.menu_set.add(Menu.objects.get(code=code))

        cls.viewer = User.objects.create_user(username="viewer", password="test12345")
        cls.editor = User.objects.create_user(username="editor", password="test12345")
        cls.viewer.groups.add(role_viewer)
        cls.editor.groups.add(role_editor)

        # editor 的待办,用来验证 viewer 够不着别人的数据
        cls.todo_of_editor = Todo.objects.create(owner=cls.editor, title="甲的专属任务")

    def test_no_perm_gets_403(self):
        """无任何权限的登录用户访问列表页 → 403(@require_perm 拦截)。"""
        User.objects.create_user(username="nobody", password="test12345")
        self.client.login(username="nobody", password="test12345")
        resp = self.client.get(reverse("superbox:todo_list"))
        self.assertEqual(resp.status_code, 403)

    def test_with_perm_gets_200(self):
        """有 todo:list 权限的用户访问列表页 → 200。"""
        self.client.login(username="viewer", password="test12345")
        resp = self.client.get(reverse("superbox:todo_list"))
        self.assertEqual(resp.status_code, 200)

    def test_owner_isolation(self):
        """用户 A 看不到、也删不了用户 B 的待办(数据权限)。"""
        self.client.login(username="viewer", password="test12345")
        # 列表页里看不到 B 的待办
        resp = self.client.get(reverse("superbox:todo_list"))
        self.assertNotContains(resp, "甲的专属任务")
        # 直接构造 URL 对 B 的待办做删除 → 404(查询条件带 owner,查无此条)
        resp = self.client.post(reverse("superbox:todo_delete", args=[self.todo_of_editor.pk]))
        self.assertEqual(resp.status_code, 404)
        self.assertTrue(Todo.objects.filter(pk=self.todo_of_editor.pk).exists())

    def test_add_without_perm_gets_403(self):
        """有查看权限但没有新增权限时,POST 新增 → 403,且数据未落库。"""
        self.client.login(username="viewer", password="test12345")
        resp = self.client.post(reverse("superbox:todo_create"), {"title": "偷偷新增"})
        self.assertEqual(resp.status_code, 403)
        self.assertEqual(Todo.objects.count(), 1)   # 只有 setUp 里那条

说明两点:

  • setUpTestData 里必须先 call_command("sync_menus") 测试运行时用的是独立的测试数据库,菜单表是空的,不先同步,Menu.objects.get(code=...) 会直接报 DoesNotExist。
  • 绑权限的写法就是"角色绑定页"背后做的事Menu.objects.get(code="xxx").groups.add(role) 或反向 role.menu_set.add(...),本质都是往 Menu 和 Group 的多对多中间表里插记录。测试里直接这样写,比走页面快得多。
  • 未授权用户的越权删除返回的是 404 而不是 403:查询条件里带了 owner,"这条数据对你来说不存在"——顺带避免向攻击者暴露"这条数据确实存在"。

本章流程口诀:先理需求 → 写模型 → 写视图(@menu + @require_perm)→ 声明目录 → 配路由 → 写模板(user_perm_codes 控按钮)→ sync_menus → 角色授权 → 验证。


第 6 章 页面风格约定(照着做,就和包内页面完全一致)

先说结论:只要遵守本节几条规范,新页面就和包内页面看不出差别。 规范之后给两个 AI 提示词模板,可以让 AI 直接代劳产出风格一致的页面。

6.1 规范清单

规范 说明
必须继承 frame_base.html 内容页写 {% extends "superbox/frame_base.html" %}。页面在右侧 iframe 里打开,是独立文档,不能用 base.html(那是外层框架页)。主题恢复脚本、提示条渲染都在 frame_base 里
面板高度 最外层 .panelstyle="height:calc(100vh - 28px);"(28 = frame-body 上下 padding 14×2),面板正好填满工作区,不出滚动条也不留缝
列表页三段式 flex column 从上到下:panel-hflex:0 0 auto)→ 搜索/工具条(flex:0 0 auto)→ .scroll-areaflex:1; min-height:0)→ .pagerflex:0 0 auto)。⚠️ flex 子元素必须写 min-height:0,否则内容会把父容器撑破、页面出现双滚动条——这是最容易踩的坑
分页组件 三档:.pager 统一版(共 N 条 ‹ 1/2 › 每页[10][20][50][200] 跳页框)、.pager.pager-compact(窄列)、.pager.pager-mini(200px 极窄列:‹ [10][50] ›)。直接照抄 role_list.html(统一版)或 role_permission.html(窄列)里的现成结构
颜色只用 CSS 变量 可用变量:--primary(主色蓝 #2563eb)、--card-bg--border--border-strong--text--text-dim--ok--warn--danger不要写死色值——包内有三套主题,写死色值切暗黑主题就穿帮
操作反馈 messages.success/error(request, "..."),frame_base 自动渲染成页面顶部提示条
确认操作 删除等不可逆操作,表单加 onsubmit="return confirm('...')"
主题跟随 iframe 每页自动恢复主题(frame_base 已处理),新页面不用写任何主题相关代码

6.2 AI 提示词模板

怎么用这些提示词:

  • 适用对象:任何对话式 AI——Claude / ChatGPT / 通义千问 / Cursor 内置 AI 等都可以;
  • 提示词里已经内嵌了 6.1 的全部风格规范,AI 不需要看到你的项目就能产出风格一致的页面:开个新对话,整段复制、替换 〔…〕 占位符,发送即可;
  • 想要更精准:把你包里的 templates/superbox/role_list.html 全文一并贴给 AI 当参照样式(下面的提示词已预留了这个可选项)。

模板一:新增列表页(复制给任意 AI,替换 〔…〕 处即可)

请为我的 Django 项目写一个列表页模板,文件保存为 templates/superbox/〔页面名〕.html。

硬性要求:
1. 第一行 {% extends "superbox/frame_base.html" %},内容写在 {% block content %} 里;
2. 最外层:<div class="panel" style="height:calc(100vh - 28px);">
3. 整体是 flex column 四段式,从上到下:
   a. 标题行 <div class="panel-h" style="flex:0 0 auto;"><span class="t">〔标题〕</span>
      <div class="spacer"></div>〔右上角操作按钮〕</div>
   b. 工具条(搜索表单等):padding:12px 18px;border-bottom:1px solid var(--border);flex:0 0 auto
   c. <div class="scroll-area" style="flex:1;min-height:0;"> 内放 <table>
   d. 分页条 <div class="pager" style="flex:0 0 auto;">
4. 表格空数据显示 <tr><td colspan=N class="empty">暂无数据</td></tr>;
5. 分页条结构(照抄):
   共 {{ page_obj.paginator.count }} 条 + ‹ 上一页 + {{ page_obj.number }}/{{ page_obj.paginator.num_pages }}
   + › 下一页 + 每页 [10][20][50][200] 切换链接(当前项加 class="cur")+ 页码跳转输入框;
   上一页/下一页用 page_obj.has_previous / has_next 判断,disabled 状态用
   <span class="pager-nav disabled">;所有分页链接要带上搜索参数 q;
6. 操作按钮(新增/编辑/删除)全部包在
   {% if "〔权限码〕" in user_perm_codes %} ... {% endif %} 里做显隐;
   删除用 POST 表单 + {% csrf_token %} +;
7. 颜色只允许用 CSS 变量:var(--primary)、var(--card-bg)、var(--border)、var(--border-strong)、
   var(--text)、var(--text-dim)、var(--ok)、var(--warn)、var(--danger),禁止写死色值;
8. 所有内联样式与包内 role_list.html 风格一致(input 圆角 8px、13px 字号等)。

视图传给模板的上下文:〔列出变量,如 items(Page 对象)、page_obj、per_page、q〕。

模板二:新增表单页(role_form.html 风格)

请为我的 Django 项目写一个表单页模板,文件保存为 templates/superbox/〔页面名〕_form.html。

硬性要求:
1. {% extends "superbox/frame_base.html" %},内容写在 {% block content %} 里;
2. 最外层 <div class="panel" style="height:calc(100vh - 28px);">,内部与 role_form.html 同构:
   panel-h 标题行(含「返回」按钮,href 指回列表页)+ 表单区;
3. <form method="post">{% csrf_token %} ...</form>,表单控件带 name 属性;
4. 若视图传了 error 变量,表单顶部显示错误提示(红色,用 var(--danger));
5. 底部按钮行:主按钮 class="btn primary",返回用普通 <a class="btn">;
6. 颜色只用 CSS 变量(--primary/--card-bg/--border/--border-strong/--text/--text-dim),
   禁止写死色值;控件风格与包内 user_form.html 一致(圆角 8px、13px 字号)。

表单字段:〔列出字段名、类型、是否必填〕。
提交地址:〔action,或留空由 {% url %} 生成〕。

第 7 章 常见问题 FAQ

现象 原因与解法
登录后侧边栏是空的 sync_menus 没跑过(见 2.3 第 7 步);或当前角色没绑任何菜单;或用的是普通用户而权限没配——超管能看到全部,可先用超管排查
新写的页面打开报 403 @require_perm 的 code 和 @menu 的 code 不一致(两处必须完全相同);或没跑 sync_menus,权限码还没进库
点侧边栏没反应 / 整页跳转 模板没继承 frame_base.html——页面该在右侧 iframe 里打开,继承错了就会整页跳
切了暗黑主题,右侧页面还是白的 页面没继承 frame_base.html。主题恢复脚本在 frame_base 里,不继承它主题就回不去
给角色勾了菜单,用户还是看不到 让用户退出重新登录——权限码在登录会话里,改授权不会实时刷新已登录用户
删除/改名了 @menu,侧边栏出现诡异残留 正常现象:sync_menus 只增不减(有意设计)。开发期 TRUNCATE TABLE superbox_menu; 后重跑 sync_menus 即可
改了 urls.py 的路由 name,菜单点了 404 @menu 里的 url 是字符串,不会自动跟着路由变。把声明里的 url 同步改掉,再重跑 sync_menus
关于邮件配置 settings 里的 MAILERS 是 Django 6.1 的新邮件 API(等价旧版的 EMAIL_BACKEND,Django 7.0 将只支持新写法)。当前配置为开发用 console 后端——邮件内容直接打印到 runserver 终端,方便调试;要接真实 SMTP 时,把 BACKEND 改为 smtp.EmailBackend 并在 OPTIONS 里加 username/password 等
开发 / 演示两种模式怎么选 看下表的 DEBUG 开关说明

开发 / 演示两种模式(DEBUG 开关):

模式 表现 注意
DEBUG=True(开发) 403 页生效;404 显示 Django 调试页(列出全部路由,开发方便,但对用户不友好) 默认即是这个状态
DEBUG=False(演示) 自定义 404 生效 必须用 runserver --insecure 启动——否则 Django 不再服务静态文件,CSS/JS 全丢、页面变裸 HTML。ALLOWED_HOSTS 已配好 ['127.0.0.1', 'localhost'],不用动

第 8 章 附录

8.1 文件地图

superbox_py/
├── manage.py                  # Django 管理入口(几乎不改)
├── .venv/                     # Python 虚拟环境(已打包,含全部依赖)
├── superbox_py/               # 项目配置包
│   ├── settings.py            # 全局配置:MySQL 连接、DEBUG、LOGIN_URL、MAILERS 等(几乎不改,仅换库时改密码)
│   └── urls.py                # 主路由:把 /superbox/ 交给 superbox app(几乎不改)
├── superbox/                  # 业务 app(RBAC 全部逻辑在这里)
│   ├── models.py              # Menu 模型(目录/菜单/按钮统一一张表,M2M 绑 auth.Group)← 加功能时改
│   ├── views.py               # 全部视图 + 全部 @menu 菜单声明(权限清单的权威来源)← 加功能时改
│   ├── urls.py                # app 路由(app_name="superbox",前缀 /superbox/)← 加功能时改
│   ├── decorators.py          # @menu 菜单声明器(登记进 MENU_REGISTRY)(几乎不改)
│   ├── permissions.py         # @require_perm / has_perm_code(权限检查核心)(几乎不改)
│   ├── context_processors.py  # 注入 side_menu_tree 和 user_perm_codes 到模板(几乎不改)
│   ├── utils.py               # flatten_menu_tree 等工具函数(几乎不改)
│   ├── signals.py             # Group 创建时自动建 profile 等(几乎不改)
│   ├── tests.py               # 测试(第 5.7 节的示例追加在这里;可选)
│   ├── apps.py / __init__.py  # app 声明(几乎不改)
│   ├── admin.py               # Django 默认生成;本项目未启用 admin,保留即可
│   ├── management/commands/
│   │   └── sync_menus.py      # 菜单同步命令(幂等,只增不减)(几乎不改)
│   ├── migrations/            # 数据库迁移文件(makemigrations 自动生成,别手改)
│   └── static/                # 本 app 静态资源(几乎不改)
└── templates/superbox/        # 页面模板(frame_base.html 是所有内容页的父模板)← 加功能时新增/修改

标注说明:← 加功能时改 = 第 5 章 demo 动的就是这几处(models / views / urls + templates/superbox/);几乎不改 = RBAC 机制文件,直接照用即可。

8.2 现有权限清单速查

包内预置功能点的完整清单(「系统管理」目录 + 用户管理/角色管理/菜单管理/权限绑定 4 个页面 + 各自的按钮码)见 4.5 节的表格,此处不再重复维护,以 4.5 为准。

8.3 命令速查

命令 干什么
python manage.py makemigrations 根据模型变更生成迁移文件
python manage.py migrate 执行迁移,落库
python manage.py sync_menus 把代码里的 @menu 声明同步进菜单表(幂等,只增不减)
python manage.py createsuperuser 创建超级管理员
python manage.py runserver 启动开发服务器

8.4 后续

本手册只讲"怎么用"。想了解四件套(@menu / sync_menus / @require_perm / user_perm_codes)的内部实现,或想做二次开发(比如把 Menu 改成数据库里维护、让管理员在页面上增删菜单),请等后续的教学手册

8.5 已配置项(勿动)

settings.py 里这些项已按本包的设计配置好,只有 DATABASESPASSWORD 是日常会改的(换成你自己的 MySQL 密码),其余不要动:

配置项 现状 / 该怎么做
DATABASES 换库密码时改 PASSWORD,其余(库名 superbox、127.0.0.1:3306)保持
INSTALLED_APPS 没有 django.contrib.admin有意的(本项目不用 admin);auth 必须保留
STATIC_URL 静态资源前缀,保持默认
LOGIN_URL "superbox:login"——未登录访问统一跳登录页
X_FRAME_OPTIONS "SAMEORIGIN"——iframe 工作台必需,改了右侧页面打不开
context_processors side_menu_tree / user_perm_codes 两项别删——侧边栏树和权限码全靠它注入模板
LANGUAGE_CODE / TIME_ZONE 界面语言 / 时区,保持默认
DEBUG 默认 True,可按需切换,见第 7 章 FAQ 的"开发 / 演示两种模式"
ALLOWED_HOSTS 已配好 ['127.0.0.1', 'localhost'],本机使用不用动

SuperBox 使用手册 · 完


superbox.rar
https://wwaxt.lanzouu.com/iON1647vuaoh

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
earnm 发表于 2026-9-11 14:10
django框架还是太强了
3#
一只大菜猫 发表于 2026-9-11 14:15
4#
hiboy999 发表于 2026-9-11 16:23
5#
dommy 发表于 2026-9-11 16:38
请问部门领导看全部或者部分数据如何处理呢?
6#
JingLiu 发表于 2026-9-11 16:41
楼主把整个思路都写出来了,但好多是专业知识,还是看不懂,感谢。
7#
 楼主| xulei 发表于 2026-9-11 17:22 |楼主

flask确实有类似django的auth这样的模块,但是得自己手动集成(这就导致很多人可选项太多了,不一定用Flask login)。我没研究过Flask,真心喜欢django的ORM
8#
 楼主| xulei 发表于 2026-9-11 17:28 |楼主
dommy 发表于 2026-9-11 16:38
请问部门领导看全部或者部分数据如何处理呢?

对于数据的管理不是我项目包的范畴。项目包实现了能不能看某个菜单下的数据和可以操作哪些按钮。对于看部分数据的需求需要结合其他的字段做归类,比如人员、时间、部门等维度做控制,相当于做数据过滤然后绑定角色。(我感觉一个模块的数据分的过细反而不太利于管理,所以也没考虑这个)
9#
 楼主| xulei 发表于 2026-9-11 17:31 |楼主
JingLiu 发表于 2026-9-11 16:41
楼主把整个思路都写出来了,但好多是专业知识,还是看不懂,感谢。

其实没干啥事,就是加了个菜单级和按钮级权限管理。
10#
wuaipojieluo168 发表于 2026-9-11 19:09
先收藏,备用着
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-12 01:26

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表