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 做业务系统,权限管理绕不开。而两条常规路线都不省心:
- django.contrib.admin 很误导人。 它就是第 0 章说的那种"管理数据表"的后台,不是业务系统的权限体系——但很多初学者以为把模型注册进 admin 就算做了权限,真到做业务系统时发现完全不是一回事。
- 从头搭一套 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 条开始,一路做到第 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(菜单)。它和 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 不管你是点菜单进来的、手输地址栏、还是用工具构造请求,都得过这一关。
光写视图还不够,还得补两件事:
superbox/urls.py 里加一条路由:path("todos/", views.todo_list, name="todo_list")——地址要和上面的 url= 对上;
- 新建
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:list,todo: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.html 里 submenu 只嵌套一层),所以推荐结构就是刚做的这三层封顶: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:user,system: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_at 与 is_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(...)、不装饰任何函数——这是本包的常规写法:目录没有页面,所以没有函数可装饰。
关于 import:render / 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 同步。三个按钮视图如果少了 @menu,sync_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 里 |
|
|
|
| 面板高度 |
最外层 .panel 写 style="height:calc(100vh - 28px);"(28 = frame-body 上下 padding 14×2),面板正好填满工作区,不出滚动条也不留缝 |
|
|
|
| 列表页三段式 |
flex column 从上到下:panel-h(flex:0 0 auto)→ 搜索/工具条(flex:0 0 auto)→ .scroll-area(flex:1; min-height:0)→ .pager(flex: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 里这些项已按本包的设计配置好,只有 DATABASES 的 PASSWORD 是日常会改的(换成你自己的 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 使用手册 · 完