吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1192|回复: 18
收起左侧

[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: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 使用手册 · 完


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

免费评分

参与人数 2威望 +1 吾爱币 +22 热心值 +2 收起 理由
苏紫方璇 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
koogg + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

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

beimingyouze 发表于 2026-9-18 18:42
xulei 发表于 2026-9-14 14:37
AI开发快,维护难。精力耗费是一点不少

是,AI经常在解释为什么炒西红柿不能放东坡肉的问题
koogg 发表于 2026-9-14 09:37
xulei 发表于 2026-9-14 09:03
这个我还真的看了,个人感觉这个做的太大了,新手不会用,老手抄模块,用这个做基础继续开发的估计不会很 ...

的确不是很轻量型。但是你要权限管理到字段的确是需要相应的约束,现在的话,倒是可以都扔给AI看是否可以精简一些
earnm 发表于 2026-9-11 14:10
一只大菜猫 发表于 2026-9-11 14:15
改用flaskL
hiboy999 发表于 2026-9-11 16:23
标记一个备用
dommy 发表于 2026-9-11 16:38
请问部门领导看全部或者部分数据如何处理呢?
静水深澜 发表于 2026-9-11 16:41
楼主把整个思路都写出来了,但好多是专业知识,还是看不懂,感谢。
 楼主| xulei 发表于 2026-9-11 17:22

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

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

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

本版积分规则

返回列表

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

GMT+8, 2026-10-27 22:27

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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