十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django用户管理系统源码深度拆解:从模型设计到宝塔部署

Django用户管理系统源码深度拆解:从模型设计到宝塔部署 简介基于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_length64, uniqueTrue, verbose_name部门名称) code models.CharField(max_length32, uniqueTrue, verbose_name部门编码) parent models.ForeignKey( self, on_deletemodels.CASCADE, nullTrue, blankTrue, related_namechildren, verbose_name上级部门, ) created_at models.DateTimeField(auto_now_addTrue, 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_deletemodels.SET_NULL, nullTrue, blankTrue, related_namemembers, verbose_name所属部门, ) phone models.CharField(max_length20, blankTrue, verbose_name手机号) avatar models.ImageField(upload_toavatars/%Y/%m/, blankTrue, verbose_name头像) class Meta: db_table user这里有两个设计值得停下来看。第一部门表用了自关联外键parent可以形成树形结构支持多级部门嵌套。related_namechildren是反向查询的入口拿到一个部门对象后用dept.children.all()就能列出它的所有子部门无需额外写递归 SQL。第二User继承的是AbstractUser而不是User这样可以在不加改 Django 认证源码的前提下给用户表追加department、phone、avatar字段。on_deletemodels.SET_NULL配合nullTrue表示删除部门后该部门下的用户不会被级联删除而是将department置空这在企业场景下更安全避免误删一个部门连带清掉几十个账号。如果你把用户管理系统部署到 MySQL 8.x 上建表时默认用的是 InnoDB 引擎和 utf8mb4 字符集。但要确认settings.py中是否写对了OPTIONSDATABASES { 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_modeSTRICT_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, usernameusername, passwordpassword) 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.Userlogin_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 langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}用户管理系统{% endblock %}/title link relstylesheet href{% static css/bootstrap.min.css %} link relstylesheet href{% static css/bootstrap-datepicker3.min.css %} {% block extra_css %}{% endblock %} /head body nav classnavbar navbar-inverse navbar-fixed-top div classcontainer-fluid div classnavbar-header a classnavbar-brand href/用户管理系统/a /div {% if user.is_authenticated %} ul classnav navbar-nav navbar-right lia href#{{ user.username }}/a/li lia href{% url user_auth:logout %}退出/a/li /ul {% endif %} /div /nav div classcontainer-fluid stylemargin-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 /htmlbootstrap-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]DEBUGTrue时 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 methodget styledisplay:inline; input typenumber namepage min1 max{{ page_obj.paginator.num_pages }} stylewidth:70px; button typesubmit跳转/button /form4.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__icontainskeyword) | Q(phone__icontainskeyword) ) if dept_id: queryset queryset.filter(department_idint(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)这里是消除 N1 查询的关键。如果没有它模板里每渲染一个用户的user.department.nameDjango 就要回 MySQL 查一次部门表可能导致几十次重复查询。现在做了一次 SQL JOIN一次查询全部取回。Q(username__icontainskeyword)生成的是WHERE username LIKE %keyword%的子条件Q对象可以用|做 OR 合并。传回模板时input value{{ keyword }}要记得转义模板引擎会默认转义为#39;避免 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_okTrue) 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] fattachment; 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目录所有者是 rootWeb 进程没有写权限。即使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 在DEBUGFalse时不会自动处理静态文件和上传文件的访问如果遇到 404优先查看 Nginx 错误日志tail -f /www/wwwroot/your_project/logs/error.log日志里会明确告诉你文件路径不对还是权限不够。作为收尾技巧可以验证一下会话清理脚本是否执行。Django 不会自动删除过期的 session 记录时间一长django_session表会膨胀建议配置一个定时任务python manage.py clearsessions在宝塔的计划任务里每天凌晨执行一次这条命令会删除数据库中已过期的会话记录让系统长期运行不积累垃圾数据。本文还有配套的精品资源点击获取
返回列表