☰
Django部门管理页面实战:从零搭建组织架构全栈功能
2026/10/1 20:19:13 网站建设 项目流程

说实话,部门管理页面是我见过最容易被低估的全栈练手项目。看着就一张列表加几个表单,真做起来,模型设计、树形结构、权限控制全都要碰一遍。最近恰好帮团队把内部组织结构搬到线上,用 Django 从零搭了一套部门管理页面,整个过程走完后我最大的感觉是:入门全栈,与其跟着教程抄电商项目,不如先搞定一个可以真实上线的组织架构模块。这篇文章就把项目从设计、建模、写功能到排查问题的完整过程整理出来。如果你正在学 Django,或需要在公司里快速搭一个部门管理工具,照着这份实操笔记走,能少踩不少坑。

1. 项目整体设计:先想清楚再动手

1.1 需求拆解:部门管理页面到底要做什么

先把需求收敛清楚。部门管理页面的核心需求其实就四件事:看列表、加部门、改部门、删部门。但一旦放到真实业务里,事情会变多:部门要有层级,比如总公司下面有研发部、市场部,研发部下面又有前端组、后端组;部门要有负责人和联系电话,方便内部找人;页面要能搜索,部门多了不能靠人工滚动查找;还得防误删,尤其是上级部门下面挂着下级部门的时候。

所以我第一版就拒绝做一个只有一张数据表格的 demo,而是按真实场景设计:部门模型支持无限层级,列表可以展开显示父子关系,新增和编辑用同一个表单模板,删除操作给出提醒,再套一层简单的登录限制。这才叫管理页面,不是玩具。

1.2 为什么选 Django 做全栈

选 Django 做这个项目,不是因为“流行”,而是因为它的设计哲学跟管理后台类需求太契合了。Django 是 MVT 架构,Model 管数据、View 管业务逻辑、Template 管页面展示,一条请求的流转路径非常清晰:浏览器发请求,路由找到视图函数,视图操作模型取数据,再把数据渲染到模板返回页面。用它写 CRUD,项目结构天然规整。

和 Flask 对比的话,Flask 轻巧灵活,适合做 API 或小服务;但要做好一个完整的管理页面,你需要自己折腾 ORM、表单校验、分页、登录、后台管理,这些 Django 全部内置了。省下来的时间可以专心处理业务本身。对全栈初学者来说,Django 的约束反而是一种保护,不容易把代码写飞。

1.3 技术方案选型:版本、数据库和前端

我用的组合是:Django 4.2 LTS、Python 3.11、SQLite(开发阶段默认数据库)、Bootstrap 5(通过 CDN 引入)、原生模板语言。没有引入任何前端框架和复杂脚手架。

Django 版本建议直接用 LTS(长期支持版),4.2 到 2026 年 4 月前都有官方安全维护,项目能安稳用很久。SQLite 作为开发库几乎没有配置成本,等真正部署到 Linux 服务器时再按官方文档切 PostgreSQL,改个数据库配置加个依赖就行。前端用 Bootstrap 是因为目标页面是后台工具,美观不是第一位,稳定、快速、对齐整齐才是重点,它的栅格和表单样式足够用了。

选项选择理由
Python3.11+Django 4.2 完全兼容,类型提示体验好
Django4.2 LTS长期维护,文档全,生态稳定
数据库SQLite开发零配置,后续可平滑切换 PostgreSQL
前端Bootstrap 5后台页面重排版,CDN 引入省打包流程

2. 环境准备与项目初始化

2.1 本地环境搭建:虚拟环境不是可选项

这一步看起来简单,但我见过不少新手直接 pip install django 装到全局环境,后面装了一堆包之后依赖冲突,项目一跑就炸。正确的做法是给每个项目单独开虚拟环境。macOS/Linux 下是:

mkdir django-department && cd django-department python3 -m venv venv source venv/bin/activate pip install django python -m django --version

Windows 下激活命令是venv\Scripts\activate。激活后命令行前缀会变成(venv),说明当前已经在独立环境里。为什么要这么做?因为 Python 项目的依赖版本互相影响很常见,虚拟环境把每一个项目的依赖隔离在各自目录中,这才是符合工程习惯的做法。

另外不要图省事用太旧的 Python,Django 4.2 要求 Python 3.8 以上,我建议直接 3.10 及以上。实测下来 Python 3.11 配 Django 4.2 非常稳定。

2.2 创建项目与应用:一条命令背后的目录结构

环境准备好之后,创建项目和应用:

django-admin startproject config . python manage.py startapp department python manage.py runserver

这里有个细节值得讲:startproject config .最后一个点不能漏。这个点表示在当前目录创建项目配置文件,而不是再嵌套一层目录。加了点之后 manage.py 直接出现在项目根目录,跑起来更清爽。

startapp department会生成一个 department 目录,里面有 models.py、views.py、admin.py、migrations 等文件。一个 Django 项目由多个 app 组成,如果你后面要加用户模块、考勤模块,每个都是独立 app,这样代码按业务边界拆分,不会出现所有逻辑都堆在一个 super_app 里的噩梦。

创建完后记得在config/settings.py的 INSTALLED_APPS 里注册 department,不注册的话数据库建表和 URL 匹配都会找不到你写的模型。这是一个非常容易漏、漏了报错还很隐晦的位置。

2.3 整体开发路线图:我用的是这条推进顺序

我个人的开发顺序是这样的,参考价值比较大:先把数据模型写好并跑通迁移,再注册 Admin 后台做快速数据维护,然后写自定义列表页和表单页,最后完善权限和部署。先用 Admin 而不是直接写页面,是因为 Admin 能帮你快速确认模型字段和关联关系设计得对不对,不用等页面搭完才发现外键关联错了,白白返工。

整体流程:

  1. 建 app、配置 settings
  2. 定义 Department 模型,跑迁移
  3. 注册 admin,录入测试部门数据
  4. 写 department_list 视图 + 模板
  5. 写 department_add / department_edit / department_delete
  6. 加登录保护和页面统一布局
  7. 关闭 DEBUG,部署测试

这个顺序能保证每一步都有可运行的结果,不会有太久看不到页面的时候。

3. 数据库模型设计:部门树状结构的核心

3.1 字段设计:一张部门表需要哪些字段

部门表最核心的字段就几个:名称(name)、编码(code)、上级部门(parent)、负责人(leader)、联系电话(phone)、创建时间(create_time)、修改时间(update_time)。

from django.db import models class Department(models.Model): name = models.CharField('部门名称', max_length=50, unique=True) code = models.CharField('部门编码', max_length=20, unique=True) parent = models.ForeignKey( 'self', verbose_name='上级部门', null=True, blank=True, on_delete=models.CASCADE, related_name='children' ) leader = models.CharField('负责人', max_length=20, blank=True) phone = models.CharField('联系电话', max_length=20, blank=True) create_time = models.DateTimeField('创建时间', auto_now_add=True) update_time = models.DateTimeField('更新时间', auto_now=True) class Meta: ordering = ['code'] verbose_name = '部门' verbose_name_plural = '部门' def __str__(self): return self.name

字段设计的几个关键选择,逐一说明。

unique=True 保证了名称和编码不重复,这是部门管理的基本要求——你肯定不想出现两个“研发部”。parent 用 ForeignKey 指向自身,允许为空,空表示顶级部门。on_delete 设置为 CASCADE,表示上级部门删除时下级一并删除,这一点后面会专门讲它的风险。

我特意加了 code 字段,而不是只用 name 做主标识。原因是部门名称可能改(研发部改名成技术研发中心),但部门编码通常不变,它更稳定,适合用来做排序、同步甚至对接外部系统的唯一键。

3.2 自关联外键:树形结构的核心原理

parent 是指向自己的外键,这里面最关键的一句话是:一张表里同时存了一条树的边。每一行部门记录都通过 parent 指向自己的上一级,顶级部门的 parent 是 NULL,这样所有部门就组成了一棵以 NULL 为根、向下蔓延的树。

读取层级的常规做法有两种。第一种是递归,从根节点往下逐层查询,代码直观但会产生大量查询,这就是典型的 N+1 问题——部门一多,数据库很受伤。第二种是查询一次全部部门,在内存中用 Python 构建父子关系,树结构展示时 O(n) 完成,这是我最推荐的方式。

列表页同时拿到所有部门后,构建这样的 dict:

def build_tree(departments): tree = [] children = {} for dep in departments: children.setdefault(dep.parent_id, []).append(dep) def walk(parent_id, depth=0): nodes = children.get(parent_id, []) for node in nodes: tree.append((node, depth)) walk(node.id, depth + 1) walk(None) return tree

这个方法维护一个以父 id 为键的子部门字典,从 None 开始递归,每一层深度 +1,返回的列表每一项是 (部门对象, 层级深度)。在模板中就可以根据 depth 数值做缩进或加前缀符号,比如用几个全角空格或者一个“└──”。

3.3 迁移与后台注册:让模型真正变成数据库表

模型写好后,执行:

python manage.py makemigrations department python manage.py migrate

makemigrations 会生成一个迁移文件,记录这次模型的变更;migrate 才真正在数据库里建表。新手常犯的错误是只跑 migrate 不跑 makemigrations,或者改了模型后忘了重新生成迁移,结果数据库表和代码不一致,报出 Unknown column 之类的错误。

建议大家养成一个习惯:改完模型立刻跑python manage.py makemigrations --check,这是一个只检查是否有未生成迁移、不改动任何内容的命令,CI 里也可以放一道。

注册 Admin 后台很简单:

from django.contrib import admin from .models import Department @admin.register(Department) class DepartmentAdmin(admin.ModelAdmin): list_display = ['name', 'code', 'parent', 'leader', 'phone'] search_fields = ['name', 'code']

注册完登录/admin,就能在后台直接增删改查部门。这一步不是为了展示,是为了在开发阶段快速造数据,后面写页面时就有真实数据可看。

4. 部门管理的 CRUD 实现:完整的增删改查流程

4.1 列表页:分页、搜索和树形展示

列表页我采用函数视图(Function-Based View),因为它逻辑简单,一步步看得明白。核心逻辑三块:搜索过滤、分页、树形排序。

from django.shortcuts import render from django.core.paginator import Paginator from .models import Department def department_list(request): departments = Department.objects.all() keyword = request.GET.get('q', '').strip() if keyword: departments = departments.filter(name__icontains=keyword) tree = build_tree(departments) paginator = Paginator(tree, 20) page_number = request.GET.get('page', 1) page_obj = paginator.get_page(page_number) return render(request, 'department/list.html', { 'page_obj': page_obj, 'keyword': keyword, })

这里有个容易踩的坑:分页对象应该建立在树构建之后。如果直接对原本的模型分页,然后再构建树,分页会把同一个部门的子节点截到上一页,结构就乱了。所以代码里先 build_tree 再分页,每次分页切的是“一条一条缩进好的行”。

模板部分遍历 page_obj,并显示层级:

{% for item in page_obj %} {% with dep=item.0 depth=item.1 %} <tr> <td> <span style="margin-left: {{ depth }}px;">{{ dep.name }}</span> </td> <td>{{ dep.code }}</td> <td>{{ dep.leader }}</td> <td>{{ dep.phone }}</td> </tr> {% endwith %} {% endfor %}

缩进我直接用 style 控制 margin-left,简单有效。搜索框和分页导航再往模板里一放,一个可用的列表页就出来了。

4.2 搜索过滤:别把查询条件写死

搜索这块要说的不多,但细节值得注意:name__icontains=keyword是大小写不敏感的包含匹配,对中文没有影响但搜索英文时更友好。search_fields 在 Admin 里也会自动用来显示搜索框。

如果你搜的是编码不是名称,加一行| models.Q(code__icontains=keyword)就可以做多字段组合:

from django.db.models import Q departments = departments.filter( Q(name__icontains=keyword) | Q(code__icontains=keyword) )

注意,keyword 为空时这个 filter 不要执行,否则全表会被通配匹配扫一遍,虽然数据量小感觉不出来,但习惯一定要养成。上面代码我用 if keyword 判断了。

4.3 新增和编辑:复用同一个表单和模板

新增和编辑的逻辑高度重合,我不建议写两套,直接用一个 ModelForm 一个视图函数处理两种场景。

from django import forms from .models import Department class DepartmentForm(forms.ModelForm): class Meta: model = Department fields = ['name', 'code', 'parent', 'leader', 'phone'] widgets = { 'name': forms.TextInput(attrs={'class': 'form-control'}), 'code': forms.TextInput(attrs={'class': 'form-control'}), 'parent': forms.Select(attrs={'class': 'form-control'}), 'leader': forms.TextInput(attrs={'class': 'form-control'}), 'phone': forms.TextInput(attrs={'class': 'form-control'}), }

widgets 给每个字段加上 Bootstrap 的 form-control 样式类,这样渲染出来的表单能直接用,不用在模板里手写每个 input 的 class。视图函数通过判断有没有传 id 来决定是新增还是编辑:

from django.shortcuts import redirect, get_object_or_404 from .forms import DepartmentForm def department_edit(request, pk=None): if pk: department = get_object_or_404(Department, pk=pk) page_title = '编辑部门' else: department = Department() page_title = '新增部门' if request.method == 'POST': form = DepartmentForm(request.POST, instance=department) if form.is_valid(): form.save() return redirect('department_list') else: form = DepartmentForm(instance=department) return render(request, 'department/form.html', { 'form': form, 'page_title': page_title, })

ModelForm 的好处是自动从模型读字段做校验,必填字段、长度限制、unique 冲突这些都不用手写。is_valid() 返回 False 时,模板里 form.errors 会自动带上错误信息,用户能直接看到哪里填错了。

表单页模板里必须放一个{% csrf_token %},这是 Django 的 CSRF 防护要求。不加的话 POST 请求会被拒绝,报 403 错误,这是新手必踩的坑之一。

4.4 删除操作:Django 查询与删除对象的正确姿势

删除功能的正确写法,加上校验和提醒。先看视图:

def department_delete(request, pk): if request.method == 'POST': department = get_object_or_404(Department, pk=pk) department.delete() return redirect('department_list') return redirect('department_list')

删除用 POST 请求而不是 GET 链接,这是安全规范。原因很简单:搜索引擎爬虫、预加载机制、甚至用户手滑点到链接都会发起 GET,GET 就能删数据,误删概率大大增加。用 POST 并且明确在页面上放“确认删除”按钮,虽然多了点步骤,但安全上的收益非常大。

关于“django 执行查询 - 删除对象”,很多人会混淆两种删除方式。第一是调用单个实例的 delete(),上面代码就是这种;第二是 Queryset 的 delete(),比如Department.objects.filter(name='临时部门').delete(),它会一次性删除整批记录。

两种方式都会返回一个元组,形如(4, {'department.Department': 3, 'department.User': 1}),第一个数字是删除的总数,第二个字典记录了按模型分组删了多少条。这在删除前做检查、删除后做日志时很有用。

但真正让我改代码的是这个问题:on_delete 模型里定义了 CASCADE,删除上级部门时下级部门会一起被删光。业务里的人事页面上,这种“级联连带”常常不是期望行为,万一主管手一快删除事业部,底下二十个小组全没了,找都找不回来。我建议业务上改成“有下级部门时禁止删除”,在视图里加一层校验:

if department.children.exists(): # 有下级部门,不能直接删 messages.error(request, '该部门下还有下级部门,请先处理下级部门') return redirect('department_list')

这才是生产环境该有的删除逻辑。如果确实需要保留历史数据,更稳妥的方案是软删除:给模型加 is_active 字段,删除时只把 is_active 置为 False,查询时默认过滤掉。硬删除(物理删除)会让历史记录跟着丢,报表、考勤、审批流如果引用了部门外键,数据完整性会出大问题。

4.5 页面统一和模板继承:不写重复的前端代码

页面要统一,必须用模板继承。base.html 放公共头部、导航、底部和一个{% block content %}占位,具体页面只需要重写 content 里的内容。

<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>{% block title %}部门管理{% endblock %}</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="{% url 'department_list' %}">组织架构管理</a> </div> </nav> <div class="container mt-4"> {% block content %}{% endblock %} </div> </body> </html>

列表页继承 base:

{% extends 'base.html' %} {% block content %} <div class="d-flex justify-content-between mb-3"> <h3>部门列表</h3> <a href="{% url 'department_add' %}" class="btn btn-primary">新增部门</a> </div> <!-- 搜索框、表格、分页 --> {% endblock %}

用 Bootstrap 的栅格和表格结构,页面不花哨但在内部工具里够看了。模板继承让所有页面公共的部分只写一遍,后面调整导航栏时改 base.html 一处就生效,这个习惯比省几个标签重要得多。

5. 权限控制与后台加速:登录态和快速维护

5.1 登录保护:用 Django 自带的认证先挡一层

部门管理页面属于内部工具,不能所有人访问,至少得登录。Django 自带用户体系和登录视图,最省事的是在视图函数上加装饰器:

from django.contrib.auth.decorators import login_required @login_required def department_list(request): ...

然后在 settings.py 里设置 LOGIN_URL:

LOGIN_URL = '/accounts/login/'

没登录的用户访问部门列表会被重定向到登录页。登录表单不用自己造,Django 自带的django.contrib.auth.views.LoginView就能渲染登录页,你只需要提供一个模板。登录成功后框架会在浏览器的 cookie 里写入 sessionid,后续请求都带着这个 cookie 识别用户身份,不需要手动去设置 token——Django 的 session 机制已经把这件事封装好了。

如果你做的是前后端分离、用 JWT 保持登录,中间逻辑会不一样,但当前这个页面是服务端渲染,把 session/cookie 链路理清楚就够了。

5.2 Admin 后台:用 Django 自带后台做快速数据维护

Django Admin 是免费附赠的管理后台,很多内部工具不加权限控制直接用 Admin 就够了。上面注册了 DepartmentAdmin,进入 /admin 就能看到部门表,点进去可以增删改查。

想让 Admin 更好用,可以把常用配置加上。list_display 控制列表显示的列,search_fields 控制搜索框能搜哪些字段,list_filter 加一个按上级部门过滤的筛选器:

@admin.register(Department) class DepartmentAdmin(admin.ModelAdmin): list_display = ['name', 'code', 'parent', 'leader', 'phone', 'update_time'] search_fields = ['name', 'code'] list_filter = ['parent']

如果觉得原生 Admin 样式太朴素,可以装django-unfold,它是目前社区里比较火的 Admin 换皮方案,提供侧边栏、现代表格样式和暗色主题,装上之后不用改业务代码,后台观感能上一个台阶。具体安装步骤官方文档都有,这里不展开。不过要提醒一句:Admin 适合内部少量用户直接维护数据,不适合直接对业务用户开放,业务页面还是要按第 4 部分的逻辑自己写。

5.3 从后台到自定义页面的取舍:哪些情况需要自己写

既然 Admin 这么好用,为什么还要自己写页面?第一,Admin 的布局是面向维护人员的,字段一堆全堆在表单里,业务用户容易犯晕;第二,Admin 没法和你的业务界面样式统一;第三,真实场景往往需要把写好的数据和其他模块联动,比如部门列表旁边要有在职人数统计、组织架构图、甚至实时推送部门变更通知。这些都需要自定义页面。

如果你的目标是快速给团队一个能用的工具,那我的建议是先跑通 Admin,再按需写几个自定义页面,不要一上来就全部自研。等需求逐渐明确,再一步步从 Admin 迁移到自定义视图,这是性价比最高的路径。

6. 实操中的常见问题与排查技巧

6.1 报错速查表:十个高频问题一次说清

这块整理成表格方便你对照:

报错 / 现象常见原因解决办法
Unknown column 'department.dep.name'改了模型没做迁移,数据库表结构旧执行 makemigrations + migrate
TemplateDoesNotExist模板目录配置不对或 INSTALLED_APPS 没注册 app检查 settings 的 DIRS 和注册信息
CSRF token missing or incorrect表单里没加 {% csrf_token %}模板中补上
403 ForbiddenCSRF 校验失败或权限不足检查 csrf 令牌与登录状态
301 跳转一直回到列表页LOGIN_URL 配置成列表页自己,登录后死循环检查登录 URL 和相关 redirect 位置
中文显示为乱码页面编码或数据库连接未用 utf8mb4settings 中设置字符集,编辑器统一 UTF-8
IntegrityError: UNIQUE constraint failedname 或 code 重复,唯一约束没通过表单校验时给用户提示,不能直接抛 500
查询结果为 None 再调用 childrenparent 为 null 时调用关系管理器先判空或确保用正确的查询链
TimeZoneWarning 时间差Django 默认 UTC,本地时间对不上settings 里设置 TIME_ZONE = 'Asia/Shanghai', USE_TZ 按需
分页参数 page 传非数字手输 ?page=abcPaginator.get_page 已经处理,不会崩,但要核实

6.2 调试习惯:用 shell 和日志快速定位

排查问题时我最常用的工具不是 IDE 断点,而是 Django 的 shell。在项目根目录跑python manage.py shell,可以直接执行模型的增删改查,配合一段脚本模拟用户的查询路径,定位问题比频繁改代码重启服务快得多。

比如你怀疑列表页树构建有问题,在 shell 里创建两个上下级部门,再调 build_tree 看输出:

from department.models import Department root = Department.objects.create(name='总公司', code='HQ') child = Department.objects.create(name='研发部', code='RD', parent=root) departments = Department.objects.all() print(build_tree(departments))

另一个习惯是在视图里临时加print(request.GET, request.user)看请求参数。别小看 print 调试,页面报错时先把请求上下文打出来,很多问题答案就在其中。

6.3 部署上线:关闭 DEBUG 只是开始

开发完成要上线时,第一步就是DEBUG = False,不做这一步直接暴露的是完整的错误堆栈和配置细节,属于安全底线。然后要让 Django 项目跑在一个正式服务器环境里,常见组合是 Nginx + Gunicorn + Django,之前的 SQLite 数据库建议换成 PostgreSQL。

静态文件也要处理。本地开发时 Django 自动帮你服务静态文件,生产环境下不会,必须执行python manage.py collectstatic把 CSS、JS、图片收集到一个目录,再让 Nginx 直接服务。这一步遗漏的后果是页面样式全部丢失。

我用 gunicorn 启动 Django 进程的命令大致是:

gunicorn config.wsgi:application --bind 0.0.0.0:8000

关于重启服务和日志查看,生产环境还有一堆细节,但部门管理的核心功能已经在第 3、4 节全部覆盖,部署只要按官方文档别抄错误博客就没大问题。

最后分享一点个人经验。部门管理页面说实话不是高难度项目,但它把全栈链路完整走了一遍:从模型设计开始,你会认真想字段够不够用;写到删除按钮时,你会琢磨误删怎么防;跑到上线,还要面对静态文件和数据库切换。这些小问题单独拎出来都不难,连在一起正是全栈真正的门槛。如果手头有这个需求,别急着去买现成系统,先自己动手写一版——等你写出来,你对于 Django 的实际掌握会比刷一个月教程都扎实。我个人最推荐的后续扩展方向是两个:一个是给部门列表加上 WebSocket 推送,让组织架构变更实时通知到前端页面;另一个是导出 Excel 报表。都很有意思,等下次有空我再专门写写。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询