ARTICLE DETAIL

资讯详情

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

Django 4.2全栈实战:从零构建部门管理增删改查系统

Django 4.2全栈实战:从零构建部门管理增删改查系统 做部门管理页面几乎是每个Django全栈项目绕不过去的一道开胃菜。单看功能无非是一张部门表加上增删改查但真动手之后你会发现这个“简单”页面背后串着Django最核心的一套机制URL怎么路由、视图怎么取数、模板怎么渲染、POST数据怎么安全落地以及一次点击从浏览器到数据库再回来的完整生命周期。这篇文章我就基于稳定版的Django 4.2从零到一把部门管理页面完整做一遍包括环境搭建、模型设计、列表展示、新增编辑、删除逻辑和页面美化也会把新手最容易踩的坑一并讲清楚。适合刚学完Django基础、想通过一个真实全栈项目把知识点串起来的朋友跟着敲一遍比看十遍文档都管用。1. 先把Django项目骨架立起来环境、项目、应用缺一不可1.1 虚拟环境与Django安装很多人一上来就pip install django然后在全局环境里开写。我建议先建虚拟环境这是第一个要养成的习惯。# 在项目目录下创建虚拟环境 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS / Linux: source venv/bin/activate # 激活后安装 Django这里固定使用 4.2 LTS 版本 pip install django4.2.16为什么要用虚拟环境我见过最典型的一个场景公司老项目还在用Django 2.2新项目要用4.2如果你全都装在全局环境里两个项目互相打架今天装这个包把另一个项目搞挂明天升级依赖又冒出一堆兼容性问题。虚拟环境就是给每个项目圈一个独立的“小房间”里面装什么版本都不影响外面。安装完之后可以用python -m django --version验证一下能看到4.2.16就说明环境没问题。这里注意一个小细节Windows下如果django-admin命令找不到通常是虚拟环境的Scripts目录没进PATH直接报“django-admin不是内部或外部命令”。这时候不用慌用python -m django是等效的后面创建项目就用这个方式。1.2 创建项目与应用理解Django的“项目-应用”双层结构环境就绪后开始创建项目和应用。我假设这个部门管理页面将来会扩展成一套企业后台所以项目名起得通用一点叫company_system。# 创建项目 django-admin startproject company_system # 进入项目目录 cd company_system # 创建部门应用 python manage.py startapp department创建完之后的目录结构是这样的company_system/ ├── manage.py ├── company_system/ # 项目配置、入口 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── department/ # 应用真正的业务代码 ├── migrations/ ├── __init__.py ├── admin.py ├── apps.py ├── models.py ├── tests.py └── views.py很多新手不理解为什么Django要搞出“项目”和“应用”两层。打个比方项目像一个公司总部负责统一考勤制度、门禁权限、公共资源应用则像各个业务部门比如人事部、财务部、行政部各自干各自的活。你这个company_system项目将来可以挂很多应用——department部门、employee员工、attendance考勤等等每一个应用都是相对独立的业务模块方便复用。创建完应用第一件事是去company_system/settings.py里注册不然Django根本不知道这个应用存在INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, department.apps.DepartmentConfig, # 新增这一行 ]顺便把语言和时区改成中文环境否则后面后台管理页面全是英文日期时间也不对LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai都配好之后运行python manage.py runserver 8000浏览器打开http://127.0.0.1:8000/看到Django的火箭欢迎页项目骨架就通了。2. 部门表的字段设计先把数据的“长相”定下来2.1 定义Department模型项目跑通只是开始真正的核心是数据模型。所谓模型就是你用Python类来描述“部门”这个业务对象在数据库里长什么样。打开department/models.py我把部门表做成了这样from django.db import models class Department(models.Model): name models.CharField(部门名称, max_length50, uniqueTrue) code models.CharField(部门编号, max_length20, uniqueTrue) leader models.CharField(负责人, max_length20, blankTrue) established_date models.DateField(成立日期, nullTrue, blankTrue) status models.SmallIntegerField( 状态, choices((1, 启用), (0, 停用)), default1 ) description models.TextField(部门描述, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) def __str__(self): return self.name class Meta: verbose_name 部门 verbose_name_plural 部门 ordering [id]字段为什么这么定我逐个说下考量的逻辑name部门名称用CharField最大长度50。这里加了uniqueTrue因为一个公司里部门名称不应该重复这也是数据库层面的最后一道防线。code部门编号比如D001。同样唯一。很多公司除了名称还会给部门一个内部编码方便后面和考勤、财务等系统做接口对接。leader负责人纯文本就够了。established_date成立日期用DateField允许为空。做报表或者按时间筛选时这个字段很有用。status状态用SmallIntegerField配合choices参数。为什么不直接存中文字符串因为后端排序、筛选、统计都更喜欢数字而且避免“启用”“启 用”“启用”这种脏数据显示层的转换交给Django的get_status_display()。description描述长文本用TextField。created_at和updated_at一个自动记录创建时间一个自动记录最后修改时间做数据审计时几乎是必备字段。这里再提醒一句写模型的时候给每个字段加上第一个位置参数作为verbose_name比如部门名称这样后面Django Admin后台、表单校验错误提示都能直接显示中文否则满屏的name、code看着很费劲。2.2 数据库迁移让模型变成真正的表模型写完之后数据库里还没有对应的表需要执行迁移命令。这是新手最容易跳过的步骤——模型改了不迁移然后运行报错no such column: department_department.name就开始怀疑人生。# 生成迁移文件相当于把模型改动记录成一份“施工图纸” python manage.py makemigrations department # 执行迁移根据图纸真正去数据库建表 python manage.py migratemakemigrations会在department/migrations/目录下生成一个类似0001_initial.py的文件它记录了“要创建哪些表、哪些字段”不影响数据库真正执行建表操作的是migrate。这两条命令的顺序不能反更不能只跑一半。这一步做完可以进入Django自带的后台管理页面看一眼地址是http://127.0.0.1:8000/admin/不过需要先创建管理员账号python manage.py createsuperuser然后在department/admin.py里把模型注册进去from django.contrib import admin from .models import Department admin.site.register(Department)再刷新后台就能在页面上手动增删部门数据了。虽然我们后面要自己写页面但这段操作很值得做它让你先确认模型本身没问题再动手写视图和模板。如果模型都没建对后面调试会非常痛苦。3. 从数据库到浏览器列表页要把这条链路走通3.1 配置URL用户请求怎么找到对应的视图部门管理页面首先要解决的是“列表展示”这个问题把数据库里所有部门显示到网页上。这个链路涉及三个角色URL、视图、模板缺一不可。我习惯在应用内部再建一个department/urls.py由项目根路由统一挂载这样每个应用的路由自己管不会挤成一团。先改项目根路由company_system/urls.pyfrom django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(department/, include(department.urls)), # 新增 ]再新建department/urls.pyfrom django.urls import path from . import views urlpatterns [ path(, views.department_list, namedepartment_list), ]这里有一个关键的编码习惯给每个URL起一个name。这样模板和视图里都用name来反向解析而不是硬编码URL字符串。以后如果URL路径改了比如从/department/改成/dept/只要name不变所有引用处自动跟着变不用满项目去搜索替换。3.2 视图函数ORM帮你把Python翻译成SQL接着写视图department/views.pyfrom django.shortcuts import render from .models import Department def department_list(request): departments Department.objects.all() return render( request, department/list.html, {departments: departments} )就这么几行。Department.objects.all()返回的是一个QuerySet它相当于一个懒加载的“查询结果集”。注意这行代码执行的时候Django并不会立刻去数据库查数据真正触发SQL执行的是模板里{% for %}循环遍历它的那一刻。这种惰性机制的好处是你可以先构造复杂的查询条件最后再去取数据数据库只执行一次最终SQL。如果你想只显示启用状态的部门可以这样写departments Department.objects.filter(status1).order_by(id)filter相当于SQL的WHEREorder_by相当于ORDER BY。这些链式调用最终被ORM合并成一条SQL语句你基本不用手写原生SQL这就是Django全栈开发效率高的原因之一。3.3 模板渲染Django模板语法怎么把数据铺到页面上在department应用下建templates/department/目录然后创建list.html。模板文件放在应用内部的templates目录是Django的一个约定不需要额外配置就能被找到。!DOCTYPE html html langzh-hans head meta charsetUTF-8 title部门管理/title /head body h1部门列表/h1 table border1 cellpadding8 cellspacing0 thead tr th序号/th th部门名称/th th部门编号/th th负责人/th th状态/th th成立日期/th th操作/th /tr /thead tbody {% for d in departments %} tr td{{ forloop.counter }}/td td{{ d.name }}/td td{{ d.code }}/td td{{ d.leader|default:— }}/td td{{ d.get_status_display }}/td td{{ d.established_date|date:Y-m-d }}/td td a href#编辑/a a href#删除/a /td /tr {% empty %} tr td colspan7暂无部门数据/td /tr {% endfor %} /tbody /table /body /html这里说几个模板语法里容易被忽略的点。{{ forloop.counter }}是循环计数从1开始省得在视图里手动算序号。{{ d.leader|default:— }}用管道符调用模板过滤器。leader字段允许为空如果直接输出None很难看用default过滤器兜底。{{ d.get_status_display }}这个特别重要。模型里status定义时带了choicesDjango会为这种字段自动生成一个get_字段名_display方法返回choices里对应的中文标签。如果你直接写{{ d.status }}页面上只会显示1或0用户根本看不懂。{{ d.established_date|date:Y-m-d }}是日期格式化。DateField在模板里默认输出的是一长串时间格式用date过滤器切成2024-06-01这种样子更清爽。此时刷新http://127.0.0.1:8000/department/配合之前在后台录入的那几条测试数据列表页就能看到东西了。页面还很简陋但这不重要先把数据链路跑通美化是后面的事。4. 新增和编辑部门表单提交背后的数据流列表页只能看不能改那是个半成品。接下来做新增和编辑。这两个功能可以拆成两个视图但我更喜欢共用一个模板因为新增和编辑的页面结构几乎一样只是编辑时要把已有数据回显到表单里。4.1 表单的name属性后台取数的钥匙先新建一个表单页面department/templates/department/form.html!DOCTYPE html html langzh-hans head meta charsetUTF-8 title{% if department %}编辑部门{% else %}新增部门{% endif %}/title /head body h1{% if department %}编辑部门{% else %}新增部门{% endif %}/h1 {% if error %} p stylecolor: red;{{ error }}/p {% endif %} form methodpost action {% csrf_token %} p label部门名称/label input typetext namename value{{ department.name|default: }} maxlength50 required /p p label部门编号/label input typetext namecode value{{ department.code|default: }} maxlength20 required /p p label负责人/label input typetext nameleader value{{ department.leader|default: }} maxlength20 /p p label成立日期/label input typedate nameestablished_date value{{ department.established_date|date:Y-m-d|default: }} /p p label状态/label select namestatus option value1 {% if department.status 1 %}selected{% endif %}启用/option option value0 {% if department.status 0 %}selected{% endif %}停用/option /select /p p label部门描述/label textarea namedescription rows4 cols40{{ department.description|default: }}/textarea /p button typesubmit保存/button /form pa href{% url department_list %}返回列表/a/p /body /html前端表单里每一个input、select、textarea上的name属性就是后端从request.POST里取数的钥匙。比如input namename后端就用request.POST.get(name)拿到用户输入的值。name写错了后端取到的就是None这是前后端联调时最高频的失误点没有之一。模板里的{% if department %}判断是用来区分新增和编辑的。编辑时传入的department对象有值所以表单里的value{{ department.name }}能回显出原有数据新增时没有这个对象department.name为空显示空白。这里用{{ department.leader|default: }}也是防止字段为空时在模板里输出None。4.2 新增视图POST请求进来就创建一条数据回到department/views.py写新增视图from django.shortcuts import render, redirect from django.urls import reverse from .models import Department def department_add(request): if request.method POST: name request.POST.get(name, ).strip() code request.POST.get(code, ).strip() # 后端必须再校验一次不能依赖前端的 required if not name or not code: return render(request, department/form.html, { error: 部门名称和部门编号不能为空 }) Department.objects.create( namename, codecode, leaderrequest.POST.get(leader, ).strip(), established_daterequest.POST.get(established_date) or None, statusrequest.POST.get(status, 1), descriptionrequest.POST.get(description, ).strip(), ) return redirect(department_list) return render(request, department/form.html)几点心得第一request.method POST判断这是区分“展示空表单”和“处理提交数据”的关键。第一次打开新增页面是GET请求返回空表单填完点保存是POST请求走创建逻辑。第二后端校验不能省。前端input required只是用户体验懂技术的人可以绕过直接往你的接口POST脏数据。这里至少校验了必填字段真实项目里还可以用unique约束兜底。第三established_daterequest.POST.get(established_date) or None这段。input typedate没填时提交过来的是空字符串但DateField不接受空字符串只接受None所以要把空串转成None否则会报日期格式错误。第四创建数据用Department.objects.create()它会直接返回一个新对象并写入数据库。等价写法是先实例化再手动save()department Department(namename, codecode) department.save()两种都行create()更简洁。最后用redirect(department_list)跳回列表页。注意是redirect而不是什么也不做这涉及一个很重要的Web开发规范下一节详细说。4.3 编辑视图先查出这条数据再覆盖保存既然要编辑URL里就得带上目标部门的ID。给department/urls.py加一条路由path(edit/int:pk/, views.department_edit, namedepartment_edit),int:pk是Django路径转换器意思是URL里这一段必须是整数会被自动捕获到视图函数的pk参数里。比如/department/edit/3/就是编辑ID为3的部门。编辑视图def department_edit(request, pk): department Department.objects.get(pkpk) if request.method POST: department.name request.POST.get(name, ).strip() department.code request.POST.get(code, ).strip() department.leader request.POST.get(leader, ).strip() department.established_date request.POST.get(established_date) or None department.status request.POST.get(status, 1) department.description request.POST.get(description, ).strip() if not department.name or not department.code: return render(request, department/form.html, { department: department, error: 部门名称和部门编号不能为空 }) department.save() return redirect(department_list) return render(request, department/form.html, {department: department})新增用的create()一步到位编辑则是“先查出来再改属性最后save()”。为什么因为编辑是修改已有记录必须先把这个对象从数据库里捞出来放到内存里改完属性后save()会执行SQL的UPDATE语句而create()是直接就INSERT。这里有个get和filter的细节新手非常容易混淆Department.objects.get(pkpk)返回一个对象。如果查询不到会抛DoesNotExist异常如果查到多条抛MultipleObjectsReturned。因为没有filter的“兜底”直接get是有风险的操作但配合URL里唯一的pk一般没问题。Department.objects.filter(pkpk)返回QuerySet查不到是空列表、查到多条也正常。想拿单个对象时通常写.first()取不到返回None。实战中我更推荐在请求进入编辑视图前把“查不到”的情况拦截掉。用Django提供的快捷函数from django.shortcuts import get_object_or_404 def department_edit(request, pk): department get_object_or_404(Department, pkpk) # ... 后续代码不变这样如果ID不存在直接返回404页面而不是抛出一个难看的异常。后面删除视图我也会用这个函数。4.4 CSRF防护和Post/Redirect/Get模式不知道你注意到没有那个表单里我写了{% csrf_token %}。这一行去掉会怎样点击保存直接给你返回403 Forbidden。这是Django默认开启的跨站请求伪造防护用来确认“这个POST请求确实来自你的浏览器页面”。原理简单说服务端在返回表单时往页面里塞了一个一次性的token用户提交POST时浏览器会把这个token一起带上服务端校验通过才执行。攻击者可以构造一个恶意页面向你的站点POST数据但他拿不到那个token就会被拦截。刚入门的人经常忘记加这个标签看到403第一反应是“服务器坏了”其实十有八九就是少了{% csrf_token %}。再回到redirect的问题。假设创建完这条数据后你不跳转而是直接重新渲染列表页。这时候用户按一下F5刷新浏览器会很贴心地提示“确认重新提交表单”如果用户点了确认同样的数据又被插入一遍。解决这个问题的标准做法叫“Post/Redirect/Get”简称PRGPOST提交完数据后服务器返回一个302重定向强制跳转到GET请求的页面。刷新时浏览器执行的是GET请求不会重复提交。所以后端保存成功后请务必return redirect(...)。5. 删除部门从点击按钮到数据库行消失5.1 为什么删除要用POST而不是GET链接列表页刚才是把“删除”写成一个a链接这只是占位。真做删除功能时千万别用GET链接直接删除数据。原因其实不难理解a链接本质是GET请求它会被浏览器预加载、被各种插件扫描、被搜索引擎爬虫抓取也会保存在浏览器历史里。万一哪天你在管理后台不小心让搜索爬虫爬到了一条/department/delete/3/的链接那等于公开允许任何人通过GET请求删你的数据代价太大了。正确的做法是每个删除按钮都包在一个独立的小表单里用POST提交。form methodpost action{% url department_delete d.id %} styledisplay: inline; {% csrf_token %} button typesubmit onclickreturn confirm(确定要删除【{{ d.name }}】吗)删除/button /formJava配了一段confirm()点击删除时先弹一个确认框用户确认后才真正提交。这个交互成本很值得毕竟删除不可逆转加一层“是不是手滑了”的判断很有必要。5.2 删除视图与Django的查询API继续在department/urls.py加路由path(delete/int:pk/, views.department_delete, namedepartment_delete),删除视图from django.shortcuts import get_object_or_404, redirect def department_delete(request, pk): department get_object_or_404(Department, pkpk) if request.method POST: department.delete() return redirect(department_list)这里用了get_object_or_404如果ID不存在直接404省去手写try/except的样板代码。department.delete()就是ORM执行删除操作对应SQL里的DELETE FROM department_department WHERE id...。关于Django的查询API趁着这个项目串一下常用方法方法作用返回类型all()查所有记录QuerySetfilter(**条件)按条件筛选QuerySetexclude(**条件)排除符合条件的QuerySetget(**条件)查单条记录查不到报错对象order_by(字段)排序QuerySet.delete()删除记录无.count()统计行数整数实际项目里比较常见的组合是Department.objects.filter(status1).order_by(id)查所有启用部门并按ID排序。这些方法可以一直链式调用下去Django的ORM会把它们翻译成一条完整的SQL。先熟悉all/filter/get/delete这四兄弟剩下的边用边查就行。5.3 完整复盘一次请求的生命周期部门管理的增删改查都齐了我建议养成一个习惯每次写完一个功能闭上眼睛走一遍“一次请求的生命周期”。以刚才删除为例完整链路是这样的用户在列表页点击“删除”按钮浏览器被confirm弹窗拦一道。用户点确认后表单以POST方式提交到/department/delete/3/。Django先执行URL路由匹配从department/urls.py里找到department_delete视图并把pk3作为参数传进去。视图执行get_object_or_404(Department, pk3)ORM翻译成SELECT * FROM department_department WHERE id3;把这条记录加载成Python对象。视图调用department.delete()ORM翻译成DELETE FROM department_department WHERE id3;。视图返回redirect(department_list)浏览器收到302响应跳转到列表页。列表页视图执行Department.objects.all()重新查询剩余部门渲染成HTML返回浏览器。你会发现整个过程的每一步都对应着一个你写过的文件路由、视图、模型、模板。通了这一条链路Django的MTV架构基本就吃透了。这也是为什么我一直觉得“部门管理”这种看似简单的小项目其实是理解Django请求闭环最好的练手材料。6. 页面美化与静态文件全栈的“前端”部分也要拿得出手6.1 静态文件配置业务功能完成之后那个列表页长着一张“裸奔”的脸——白底黑字带边框实在说不上能用。这个项目既然叫“全栈”前端的门面也得管起来。Django对静态文件CSS、JS、图片有一套固定管理方式。先确认settings.py里这两项配置STATIC_URL /static/再在项目根目录建一个static/文件夹并告诉Django去哪找它STATICFILES_DIRS [ BASE_DIR / static, ]STATIC_URL是浏览器访问静态文件时用的URL前缀比如http://127.0.0.1:8000/static/css/style.cssSTATICFILES_DIRS是告诉Django“开发环境下去哪些目录找静态文件”。这两个概念实际部署时还会有一个python manage.py collectstatic收集所有静态文件的步骤开发阶段先不用管。6.2 写一份轻量CSS把页面撑起来在没有引入前端框架的前提下手写一份够用的CSS完全可行。我在static/css/style.css里写了一套简单的后台管理风样式不用Bootstrap也能把页面做得简洁干净body { font-family: Microsoft YaHei, PingFang SC, sans-serif; background: #f5f6fa; margin: 0; padding: 30px; } .container { max-width: 1100px; margin: 0 auto; background: #fff; border-radius: 8px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .page-header h1 { font-size: 22px; margin: 0; } table { width: 100%; border-collapse: collapse; } th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; } th { background: #fafbfc; color: #2c3e50; font-weight: 600; } tr:hover { background: #f8f9fb; } .btn { display: inline-block; padding: 6px 16px; border-radius: 4px; font-size: 14px; text-decoration: none; border: none; cursor: pointer; } .btn-primary { background: #3273dc; color: #fff; } .btn-edit { background: #f0f1f3; color: #333; } .btn-danger { background: #ff3860; color: #fff; } .error-msg { background: #fff5f5; color: #cc0f35; padding: 10px 14px; border-radius: 4px; margin-bottom: 16px; }然后回到模板头部引入它{% load static %} !DOCTYPE html html langzh-hans head meta charsetUTF-8 title部门管理/title link relstylesheet href{% static css/style.css %} /head注意两个细节第一HTML模板里要用静态文件必须先{% load static %}这是Django模板库里提供的标签。第二CSS文件路径通过{% static css/style.css %}生成而不是手写/static/css/style.css。这样以后如果想给静态文件加版本号、或者部署到CDN改配置就行模板不用动。改造后的列表页结构div classcontainer div classpage-header h1部门列表/h1 a classbtn btn-primary href{% url department_add %}新增部门/a /div table ... /table /div新增/编辑表单页也套进同一个container整体观感立刻就不一样了。6.3 再做两个小交互删除确认和报错提示删除确认之前已经加了confirm()。还有一个体验细节是表单错误提示我在form.html里留了{% if error %}的占位配合CSS里的.error-msg后端校验没通过时页面会有一段醒目的红底提示而不是默默没反应。这两个小交互看起来不起眼却是从“教学demo”走向“能交付的系统”之间的重要一步。7. 排坑笔记新手做Django CRUD最容易翻车的几个点7.1 环境与启动层的坑先说最基础但出现频率最高的问题。django-admin命令找不到通常是虚拟环境没激活或者安装目录不在PATH里用python -m django替代能绕过去。runserver 8000提示端口被占用常见于之前忘了关服务或者有其他程序占了端口换个端口python manage.py runserver 8001就行。还有Windows下打开终端发现python指向了微软商店的假命令建议直接改用py启动器或者用Anaconda自带的终端。7.2 URL与模板层的坑模板报错里最常遇到的两个是NoReverseMatch和TemplateDoesNotExist。NoReverseMatch的意思是模板里{% url 某个名字 %}中的名字在URL配置里找不到。检查步骤很固定看urls.py里每个path的name参数是否拼写一致如果是带参数的URL比如{% url department_edit d.id %}还要确认视图函数确实定义了pk参数。TemplateDoesNotExist则是Django没找到模板文件。十个里有八个是模板目录建错了位置。记住Django的查找规则它会去每个注册应用的templates目录下找。你必须在department/templates/department/list.html这种“应用内的template内再套一层应用名”的路径否则多个应用都有list.html时会互相冲突。7.3 ORM与数据库层的坑migrate没跑就启动项目会报no such table改了模型没重新makemigrations会报no such column。这两个报错的排查思路是一样的检查migrations/目录下有没有对应的迁移文件没有就先makemigrations再migrate。还有一个关于中文的坑。Django默认用的SQLite对中文支持没问题但如果生产环境换MySQL建库时没指定utf8mb4编码插入中文会报Incorrect string value。建库语句里带上这个CREATE DATABASE company_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;顺便说下Windows的PowerShell里跑Django命令如果出现中文乱码先执行chcp 65001切到UTF-8代码页再看终端里是不是用了等宽字体。7.4 表单与安全意识的坑最后单独列一张常见的“表单与安全”对照表都是实际项目里特别容易踩的问题现象解决办法忘记{% csrf_token %}POST提交报403表单内部加{% csrf_token %}前端name写错后端取到None对照后端request.POST.get(...)的名字逐字核对编辑时字段不回显页面显示空白input的value属性里填{{ 字段名 }}删除用a链接存在误删、被爬的风险改成POST表单 confirm()空字符串传给DateField日期格式报错request.POST.get(...) or None用filter().get()混用报MultipleObjectsReturned想取单条用get或filter().first()各自处理好异常这六条属于“新手翻车百发百中”的典型记住一次就能少折腾半天。我个人做完这个小项目最大的感受是这类部门管理页面技术难度并不高但它把Django全栈开发里最常用的骨架全部串起来了——模型设计、ORM查询、URL路由、视图函数、模板渲染、表单提交、CSRF防护、静态文件管理再到一次完整请求的生命周期。把这些想明白之后再去看Django文档里那些进阶概念比如类视图、表单类、分页、权限控制理解成本会低很多。如果有条件建议你在这个项目基础上继续加东西比如部门搜索、分页、或者给部门加层级关系做成树形结构都是很自然的功能扩展。能把这一个模块吃透你的全栈之路就算正式迈出第一步了。
返回列表