简介:基于Django与MySQL构建的用户管理系统源码,适合Web开发初学者和需要快速搭建后台管理系统的开发者。系统完整覆盖部门管理、用户管理、注册登录认证、文件上传等常用模块,有助于理解现代Web应用的权限控制、会话管理与数据交互流程,代码注释完整,对学习Django与Bootstrap整合尤为友好。资源包共166个文件,约5.55MB;其中26个Python文件实现后端逻辑,91个JavaScript文件驱动前端交互,16个HTML模板与13个CSS样式表构成界面,另含SQL脚本、字体及图片等静态资源,目录结构清晰,便于定位和二次开发。目前已有302人浏览学习,源码基于Bootstrap构建响应式后台界面,并整合Django默认认证机制,可直接运行或作为项目脚手架。通过阅读源码可掌握Django项目分层、MySQL关联查询、文件上传处理等实用技巧,适合课程设计、毕业设计及企业级用户管理模块的快速落地。
1. 这套用户管理系统,不只是个增删改查的 Demo
拿到这份源码时,我第一反应是“又一个 Django 练手项目”,但扫完目录结构后改变了判断。163 个文件里,91 个 JavaScript、23 个 Python、16 个 HTML 模板,这个配比说明前端交互和模板复用占了相当比重,而不是简单的 render 一下表格就完事。更关键的是,它把 Bootstrap Datepicker、文件上传、部门与用户两级数据模型都接进来了,是一套可以直接拿去做二次开发基座的中型项目骨架。
很多自学 Django 的人卡在“模型建好了不知道怎么串成完整业务”,比如用户注册登录后如何和部门联动、文件上传的请求怎么安全落盘、Bootstrap 组件的样式为什么有时候加载不出来。这套源码把这些问题用代码回答了一遍。本文会直接拆它的模型设计、认证流程、URL 路由映射、文件上传的存储策略,以及部署到宝塔或云主机时需要改哪些配置。无论你是准备交课程设计,还是想给公司内部搭个后台,跟着这篇把每个文件的作用摸透,比单纯把项目跑起来有价值得多。
2. 模型设计与数据库初始化:从 ORM 映射到 MySQL 存储引擎
2.1 项目目录结构与 Django App 的边界划分
解压源码包后,先用tree看整体布局,关键目录是这样组织的:
project_root/ ├── manage.py ├── templates/ # 16 个 HTML 模板集中存放 │ ├── registration/ # 注册、登录模板 │ ├── department/ # 部门管理模板 │ ├── user/ # 用户管理模板 │ └── upload/ # 文件上传模板 ├── static/ │ ├── css/ # Bootstrap 3 全系样式及 Datepicker 样式 │ ├── js/ # 91 个 JS 文件,含 Bootstrap 和日期插件 │ └── fonts/ # TrueType 字体,Glyphicons 依赖它们 ├── apps/ │ ├── department/ # 部门管理 App │ ├── user_auth/ # 用户认证 App(注册、登录、会话) │ └── file_upload/ # 文件上传 App ├── requirements.txt └── README.md可以注意到源码没有把templates和static散落到各 App 中,而是集中放在根目录。这套组织方式在 Django 2.x 到 4.x 的项目里很常见,目的是让前端资源统一管理,模板继承时{% extends "base.html" %}的路径解析更直接。如果你的项目后续要拆分给多个 App 共用布局,这种集中式的结构会省很多事。
目录里没有venv、__pycache__这类杂物,说明打包前做过清理,这一点值得学习。很多网上打包的源码捎带几百 MB 的缓存文件,既影响阅读又容易让初学的人混淆。
2.2 核心模型拆解:用户表如何与部门表建立关联
打开apps/user_auth/models.py,会看到项目的核心模型。这里我摘出最关键的字段说明设计思路:
from django.db import models from django.contrib.auth.models import AbstractUser class Department(models.Model): """部门表,独立于用户表存在,避免用户表出现大量冗余文本字段""" name = models.CharField(max_length=64, unique=True, verbose_name="部门名称") code = models.CharField(max_length=32, unique=True, verbose_name="部门编码") parent = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children", verbose_name="上级部门", ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") class Meta: db_table = "department" ordering = ["-created_at"] def __str__(self): return self.name class User(AbstractUser): """扩展 Django 内置 User,增加部门外键与手机号字段""" department = models.ForeignKey( Department, on_delete=models.SET_NULL, null=True, blank=True, related_name="members", verbose_name="所属部门", ) phone = models.CharField(max_length=20, blank=True, verbose_name="手机号") avatar = models.ImageField(upload_to="avatars/%Y/%m/", blank=True, verbose_name="头像") class Meta: db_table = "user"这里有两个设计值得停下来看。第一,部门表用了自关联外键parent,可以形成树形结构,支持多级部门嵌套。related_name="children"是反向查询的入口,拿到一个部门对象后,用dept.children.all()就能列出它的所有子部门,无需额外写递归 SQL。第二,User继承的是AbstractUser而不是User,这样可以在不加改 Django 认证源码的前提下,给用户表追加department、phone、avatar字段。on_delete=models.SET_NULL配合null=True表示删除部门后,该部门下的用户不会被级联删除,而是将department置空,这在企业场景下更安全,避免误删一个部门连带清掉几十个账号。
如果你把用户管理系统部署到 MySQL 8.x 上,建表时默认用的是 InnoDB 引擎和 utf8mb4 字符集。但要确认settings.py中是否写对了OPTIONS:
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "user_management", "USER": "root", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { "charset": "utf8mb4", "init_command": "SET sql_mode='STRICT_TRANS_TABLES'", }, } }sql_mode设置为STRICT_TRANS_TABLES后,MySQL 会在写入超长字符串或非法日期时直接报错,而不是截断后静默写入。这能帮你提早发现数据问题,否则用户表里存了一堆被截断的部门名称,排查起来非常痛苦。Python 3.8 以上连接 MySQL 需要安装mysqlclient,如果遇到mysql_config not found的报错,在 Debian/Ubuntu 上先执行:
sudo apt-get install default-libmysqlclient-dev build-essential pkg-config pip install mysqlclient2.3 执行迁移与初始数据写入
模型定义完只是第一步,真正建表靠的是 Django 的迁移系统。在项目根目录依次执行:
python manage.py makemigrations user_auth python manage.py migratemakemigrations会扫描模型的变化,生成迁移文件(类似 git 的 commit 记录),migrate才是真正把CREATE TABLE语句发送给 MySQL。如果你是从零开始导入这套源码,建议先用python manage.py createsuperuser创建一个管理员账号,方便后面登录 Django Admin 查看数据。
MySQL 中有几类存储引擎,这个项目不需要额外配,InnoDB 即可,关键是确认表名的设定。源码里通过db_table明确指定了表名,没有让 Django 用默认的appname_modelname格式,这样在 Navicat 或命令行直接查表时更容易定位:
USE user_management; SHOW TABLES; DESC user; DESC department;如果你在别的机器上导入,记得先手动创建数据库,并确认user_management的字符集:
CREATE DATABASE user_management DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;3. 用户认证与权限控制:注册、登录、会话状态是如何串起来的
3.1 注册视图的逻辑拆解
用户认证部分的入口是apps/user_auth/views.py。注册视图是这套系统里最值得读的代码之一,它展示了表单验证、模型保存和登录状态保持三者如何协同:
from django.shortcuts import render, redirect from django.contrib.auth import login, logout, authenticate from django.contrib.auth.forms import UserCreationForm from apps.user_auth.models import User def register(request): if request.method == "POST": form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() # 此时密码会被自动哈希,不存明文 department_id = request.POST.get("department_id") if department_id: user.department_id = int(department_id) user.save() login(request, user) # 注册成功后直接建立会话,无需再跳登录页 return redirect("user_auth:dashboard") else: form = UserCreationForm() return render(request, "registration/register.html", {"form": form})form.save()这一步底层调用的是User.objects.create_user(),它对密码做了make_password处理,存进 MySQL 的是pbkdf2_sha256$打头的哈希串,而不是原始密码。千万不能在这里改成User.objects.create(),否则密码明文落库,一旦数据库泄露就是安全事故。user.department_id是外键字段的隐藏 ID 列,Django 允许你直接赋值整型而不用先查一次部门对象,省了一次数据库查询。
注册成功后的login(request, user)会做两件事:一是把用户 ID 写入 session,二是调用user.backend做认证标记。如果不调用它,用户注册完还得去登录页再输一次密码,交互路径就长了一截。源码选择直接跳转dashboard,比较符合内部系统的操作习惯。
3.2 登录视图与登录装饰器的边界
登录视图做了两层防护:一是验证用户名密码,二是判断用户是否被标记为禁用。看核心代码:
def user_login(request): if request.method == "POST": username = request.POST.get("username") password = request.POST.get("password") user = authenticate(request, username=username, password=password) if user is not None: if user.is_active: login(request, user) return redirect(request.GET.get("next", "user_auth:dashboard")) else: error = "账号已被禁用,请联系管理员" else: error = "用户名或密码错误" else: error = "" return render(request, "registration/login.html", {"error": error}) @login_required(login_url="/auth/login/") def dashboard(request): user = request.user return render(request, "dashboard.html", { "user": user, "department": user.department, })authenticate()会读取 AUTHENTICATION_BACKENDS 配置,按顺序尝试认证后端,默认的ModelBackend就是用用户名和密码去查User表。这里有个细节:如果项目里自定义了AUTH_USER_MODEL,记得在settings.py开头声明:
AUTH_USER_MODEL = "user_auth.User"@login_required装饰器拦截未登录请求,login_url参数指定跳转地址。如果你想更细粒度地控制权限,比如只允许某个部门的成员访问,可以在此基础上再包一层自定义装饰器:
from django.core.exceptions import PermissionDenied def department_required(dept_code): def decorator(view_func): def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: raise PermissionDenied if request.user.department and request.user.department.code == dept_code: return view_func(request, *args, **kwargs) raise PermissionDenied return wrapper return decorator这种按部门编码鉴权的方式在内部管理系统里很实用,比如“财务部专属报表页”“人事部专属入职流程”,代码量不大但可以复用到任意视图上。
3.3 会话配置与登录态失效策略
Django 的会话默认存在数据库的django_session表里,每次请求都会解密 session_id 然后查询。源码没有改动这个默认行为,但部署到生产环境时,建议加上以下配置:
SESSION_COOKIE_AGE = 60 * 60 * 8 # 8 小时后登录态过期 SESSION_SAVE_EVERY_REQUEST = True # 每次请求都刷新过期时间,用户一直在操作就不掉线 SESSION_EXPIRE_AT_BROWSER_CLOSE = True # 关闭浏览器自动失效SESSION_SAVE_EVERY_REQUEST初始值是False,如果不设成True,用户在一个会话内持续操作超过SESSION_COOKIE_AGE后仍然会被强制登出,体验很差。这个参数是我在自己维护的后台系统里吃过亏才记住的,源码没提但你上线前必须改。
4. 部门管理与用户管理页面:从模板继承到 DataTables 交互
4.1 Bootstrap 模板继承与静态资源引用链
16 个 HTML 模板里通常有一个base.html,其余模板都继承它。看一段典型的模板头:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}用户管理系统{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'css/bootstrap-datepicker3.min.css' %}"> {% block extra_css %}{% endblock %} </head> <body> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="/">用户管理系统</a> </div> {% if user.is_authenticated %} <ul class="nav navbar-nav navbar-right"> <li><a href="#">{{ user.username }}</a></li> <li><a href="{% url 'user_auth:logout' %}">退出</a></li> </ul> {% endif %} </div> </nav> <div class="container-fluid" style="margin-top: 60px;"> {% block content %}{% endblock %} </div> <script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap-datepicker.min.js' %}"></script> {% block extra_js %}{% endblock %} </body> </html>bootstrap-datepicker3.min.css对应的是 Bootstrap 3 专用版本,如果页面加载了 Bootstrap 4 的 CSS 却配了 3 的 Datepicker,样式几乎必定错位。这套源码的文件命名很规范,standalone后缀代表日期选择器离了 Bootstrap 也能独立工作,适合嵌入到复杂布局中。
模板里的{% static %}标签依赖django.contrib.staticfiles这个内置应用,在settings.py里需要确认:
STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR / "static"]DEBUG=True时 Django 能直接伺服静态文件,但如果用python manage.py collectstatic部署到宝塔,要改STATIC_ROOT和 Nginx 的静态转发规则,这个坑在最后一部分展开。
4.2 部门列表分页与前台渲染
部门管理视图从 MySQL 里取出全部部门后,源码落在两个点上:一是通过render传给模板,二是支持分页。分页代码是大多数项目里可以直接拷走的一段:
from django.core.paginator import Paginator def department_list(request): all_depts = Department.objects.all().select_related("parent") paginator = Paginator(all_depts, 10) # 每页 10 条 page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) return render(request, "department/department_list.html", {"page_obj": page_obj})Paginator把 QuerySet 切片成页,get_page()会自动捕获越界页码并返回最后一页或第一页,不会抛 404。模板层配套的翻页控件:
{% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}">上一页</a> {% endif %} <span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span> {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}">下一页</a> {% endif %}这里我一般会加一个跳转输入框,直接指定页码跳到对应页,不然几百个部门时翻页很痛苦:
<form method="get" style="display:inline;"> <input type="number" name="page" min="1" max="{{ page_obj.paginator.num_pages }}" style="width:70px;"> <button type="submit">跳转</button> </form>4.3 用户列表的条件筛选与表格渲染
用户管理页面通常比部门页复杂,因为用户字段多,还要支持按部门、按关键字筛。核心视图长这样:
def user_list(request): queryset = User.objects.select_related("department").all() keyword = request.GET.get("keyword", "").strip() dept_id = request.GET.get("department_id", "").strip() if keyword: queryset = queryset.filter( Q(username__icontains=keyword) | Q(phone__icontains=keyword) ) if dept_id: queryset = queryset.filter(department_id=int(dept_id)) page_obj = Paginator(queryset, 8).get_page(request.GET.get("page")) return render(request, "user/user_list.html", { "page_obj": page_obj, "departments": Department.objects.all(), "keyword": keyword, "dept_id": dept_id, })select_related("department")这里是消除 N+1 查询的关键。如果没有它,模板里每渲染一个用户的user.department.name,Django 就要回 MySQL 查一次部门表,可能导致几十次重复查询。现在做了一次 SQL JOIN,一次查询全部取回。
Q(username__icontains=keyword)生成的是WHERE username LIKE %keyword%的子条件,Q对象可以用|做 OR 合并。传回模板时<input value="{{ keyword }}">要记得转义,模板引擎会默认转义为'避免 XSS 注入。
用户在模板中显示的表格列通常有:用户名、姓名、所属部门、手机号、最近登录、操作(编辑/禁用)。源码用的是内联编辑弹窗还是跳转编辑页,取决于 16 个模板里有没有包含user_edit_modal.html。如果只有user_edit.html,那就是独立页面编辑,逻辑更直观。
5. 文件上传模块:请求解析、磁盘存储与访问鉴权
5.1 文件上传视图与表单编码
文件上传在 Django 里不是一个困难功能,但要写得符合生产要求还是有不少细节。视图核心片段如下:
from django.conf import settings import os def upload_file(request): if request.method == "POST": upload_file = request.FILES.get("upload_file") if not upload_file: return render(request, "upload/upload.html", {"error": "未选择文件"}) max_size = 10 * 1024 * 1024 # 10MB if upload_file.size > max_size: return render(request, "upload/upload.html", { "error": "文件大小超过 10MB 限制", }) allowed_ext = [".pdf", ".doc", ".docx", ".xlsx", ".zip"] ext = os.path.splitext(upload_file.name)[1].lower() if ext not in allowed_ext: return render(request, "upload/upload.html", { "error": "不允许的文件类型", }) upload_dir = os.path.join(settings.MEDIA_ROOT, "uploads") os.makedirs(upload_dir, exist_ok=True) dest_path = os.path.join(upload_dir, upload_file.name) with open(dest_path, "wb+") as dest: for chunk in upload_file.chunks(): dest.write(chunk) return render(request, "upload/upload.html", {"success": "上传成功"}) return render(request, "upload/upload.html")upload_file.chunks()是 Django 对文件句柄的分块迭代器,每块默认 2.5MB,避免用户上传大文件时一次性读入内存导致服务器崩溃。很多初学者会用read()直接读取全部内容,这个写法在 100MB 文件时基本会把内存打满。
这里还隐藏了一个问题:dest_path直接用原始文件名拼接,如果两个用户上传同名文件会互相覆盖。生产环境一定要加随机前缀或按日期归档:
import uuid from datetime import datetime date_prefix = datetime.now().strftime("%Y%m%d") unique_name = f"{uuid.uuid4().hex}_{upload_file.name}" dest_path = os.path.join(upload_dir, date_prefix, unique_name)5.2 MEDIA_ROOT 配置与访问路径映射
settings.py里必须有这两行:
MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media")MEDIA_ROOT是文件写入的物理路径,MEDIA_URL是浏览器访问的 URL 前缀。在开发环境,Django 通过urls.py加上一段路由来伺服用户上传的文件。但如果你部署到 Nginx 后面,/media/必须在 Nginx 里单独做一个 location 映射,指向项目的media目录,而不是让 Django 去读文件,否则性能会非常差。
如果项目要控制上传文件的访问权限,例如仅登录用户可下载,视图里需要先login_required,再通过FileResponse输出文件流,核心逻辑是:
from django.http import FileResponse @login_required def download_file(request, file_name): file_path = os.path.join(settings.MEDIA_ROOT, "uploads", file_name) if not os.path.exists(file_path): raise Http404("文件不存在") response = FileResponse(open(file_path, "rb")) response["Content-Disposition"] = f"attachment; filename={file_name}" return responseContent-Disposition是告诉浏览器“这是下载内容而不是在页面上渲染”。如果不加这个头,一个.pdf文件可能被浏览器直接打开预览而不是下载。
5.3 文件操作的排错:上传后找不到文件、文件夹权限导致写入失败
在 Linux 云服务器上部署后最常见的一个问题是:表单提交后报错Permission denied,排查步骤是先确认media目录的所有者和权限:
ls -la media/ sudo chown -R www-data:www-data media/ sudo chmod -R 755 media/www-data是 Nginx/Apache 运行时的默认用户,如果源码是用 root 用户解压的,media目录所有者是 root,Web 进程没有写权限。即使chmod 777能解决问题,也不要这样用,安全风险太大。
另一个坑是settings.py中的MEDIA_ROOT用了相对路径:
MEDIA_ROOT = os.path.join(BASE_DIR, "media")这样没有任何问题,BASE_DIR是manage.py所在目录的绝对路径。如果你在宝塔面板的 Python 项目管理器里改过家目录,或者启动命令用了--pythonpath,可能路径就错位了。验证方法很简单:
python manage.py shell >>> from django.conf import settings >>> print(settings.MEDIA_ROOT)打印出来是不是你预期的路径,一目了然。
6. 基于宝塔面板的部署验证与性能检查技巧
这部分聊聊怎么把这套系统真正跑在公网服务器上。宝塔面板是目前中小团队部署 Django 最常见的环境之一,这里给一条完整的可操作链路。
先在宝塔软件商店安装 Nginx 和 MySQL 5.7/8.0,然后在网站页面添加一个 Python 项目。选择 Python 版本为 3.8+,框架选 Django,项目目录填入源码解压路径。宝塔会自动创建一个虚拟环境,你需要在这个环境里执行:
pip install -r requirements.txt python manage.py migrate python manage.py collectstatic --noinput python manage.py createsuperusercollectstatic会把所有 App 的静态文件复制到STATIC_ROOT目录,Nginx 的伪静态配置里必须加上:
location /static/ { alias /www/wwwroot/your_project/static/; expires 7d; } location /media/ { alias /www/wwwroot/your_project/media/; }expires 7d是给静态资源加浏览器缓存,日期插件、Bootstrap 这些文件体积不小,不缓存的话每次刷新都重新下载,页面性能会明显打折。改完 Nginx 配置记得重载:
nginx -s reload然后验证两个核心 URL:/auth/login/是否能正常出登录页,以及登录后能否看到部门和用户列表。如果 CSS 样式丢失,多半是static的 alias 路径写错或者 collectstatic 没有执行。
这里再提一个排查问题的固定动作。Django 在DEBUG=False时不会自动处理静态文件和上传文件的访问,如果遇到 404,优先查看 Nginx 错误日志:
tail -f /www/wwwroot/your_project/logs/error.log日志里会明确告诉你文件路径不对还是权限不够。作为收尾技巧,可以验证一下会话清理脚本是否执行。Django 不会自动删除过期的 session 记录,时间一长django_session表会膨胀,建议配置一个定时任务:
python manage.py clearsessions在宝塔的计划任务里每天凌晨执行一次,这条命令会删除数据库中已过期的会话记录,让系统长期运行不积累垃圾数据。
本文还有配套的精品资源,点击获取