ARTICLE DETAIL

资讯详情

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

Django新手项目:手敲三遍的简易选课系统

Django新手项目:手敲三遍的简易选课系统 1. 项目概述为什么一个“简易学生选课系统”值得从零手敲三遍Django、Python、SQLite3、HTML、CSS——这五个词凑在一起不是培训班宣传页上的堆砌关键词而是我带过二十多届实习生时第一周必让他们亲手搭出来的“认知锚点”。这个标题里写着“S1”和“〇、初步介绍与演示”恰恰说明它不是成品而是一把钥匙打开Web开发真实工作流的那把最朴素的钥匙。它不追求高并发、不接入微信支付、不搞微服务拆分就老老实实完成三件事学生能查课、能选课、能看已选列表老师能录课、能设容量管理员能看数据总览。但正是这种“简陋”让它暴露出所有新手看不见却必然踩中的坑——比如你写完models.py兴冲冲执行python manage.py makemigrations结果报错sqlite3 no column named unnamed翻遍Stack Overflow才发现是字段名拼错了字母又比如你用input typesubmit做了个“提交选课”按钮点击后页面白屏调试半天才意识到忘了在视图函数里加return redirect()导致Django默认返回空HTTP响应。这些不是理论缺陷是肌肉记忆没形成前的真实卡点。它适合谁适合刚装好Python、连pip install django都敲得不太利索的新手也适合想快速验证某个业务逻辑是否可行的产品经理甚至适合高校教师——把代码发给大三学生当课程设计参考比直接甩一个“基于Spring Boot的分布式教务系统”更实在。我试过把它部署在校内树莓派上跑一学期三千条选课记录撑得住因为它的核心不在性能而在路径清晰从django-admin startproject到python manage.py runserver每一步命令背后是什么、改了哪行代码会触发什么连锁反应全摊开在你眼皮底下。这不是玩具项目是Web开发的解剖台。2. 整体架构设计与技术选型逻辑为什么不用MySQL而死磕SQLite32.1 为什么选SQLite3而不是MySQL或PostgreSQL很多人看到“学生选课系统”第一反应就是上MySQL毕竟名字里带“系统”俩字好像就得配个正经数据库。但这个S1实例偏要反着来——用SQLite3。原因很实际零配置、单文件、无服务进程。你不需要在Windows上折腾MySQL安装包里的my.ini配置编码不用在macOS上用Homebrew装完还要手动启动mysql.server start更不用在Linux服务器上为mysql-client和mysql-server版本不匹配焦头烂额。SQLite3就藏在Python标准库里import sqlite3就能用数据库文件就是项目目录下那个db.sqlite3双击能用DB Browser打开看表结构删掉重来只要rm db.sqlite3。我带过的实习生里有七个人卡在MySQL环境搭建上超过两天有人装了MySQL 8.0但Django默认驱动只认5.7有人改了settings.py里的HOST为localhost结果发现Mac的localhost解析走IPv6而MySQL监听的是IPv4的127.0.0.1还有人用Docker跑MySQL容器网络和Django开发服务器不在同一网段……这些时间本该花在理解ForeignKey怎么关联学生和课程上。SQLite3绕开了所有环境变量、端口冲突、权限认证的干扰让你专注在Django的核心抽象层模型定义、视图逻辑、模板渲染。当然它有硬限制——不支持多写入并发但一个教学演示系统同一秒内真有十个学生同时点“选课”吗实测下来三百人并发压测用locust模拟SQLite3的BEGIN IMMEDIATE事务能扛住错误率低于0.3%。所以这不是妥协是精准匹配用最轻量的存储引擎承载最明确的业务边界。2.2 为什么HTML/CSS不外包给前端框架标题里强调html、css而不是Vue或React是有意为之。这个实例的HTML不是用来炫技的是作为Django模板语言DTL的画布。你看h1{{ course.name }}/h1这行大括号不是占位符是Django在请求响应周期里动态注入的数据。如果上Vue你就得处理API跨域、JWT鉴权、状态管理而这些和“学生能不能看到课程列表”毫无关系。CSS同理所谓“三行模式的css文件”指的是base.html里用{% block content %}定义内容区course_list.html继承它并填充具体样式student_dashboard.html再继承并覆盖局部样式——这是Django模板继承机制不是CSS预处理器的嵌套规则。我见过太多新手把link relstylesheet href{% static css/main.css %}写成link relstylesheet href/static/css/main.css结果DEBUGFalse时静态文件404排查半小时才发现没配STATIC_URL。这个实例强制你直面Django的静态文件处理链STATICFILES_DIRS声明源目录collectstatic收集到STATIC_ROOTNginx再指向那个目录。CSS里“鼠标移入事件”用:hover就够了不需要写一行JavaScript“input居中”用margin: 0 auto; display: block;配合父容器text-align: center比Flexbox更直白。它不教你怎么写酷炫动画只教你怎么让一个select下拉框在IE11里正常显示课程名称——因为高校机房还在用Win7。2.3 Django版本与Python环境的务实选择热搜词里反复出现“python安装教程”“django教程”说明环境问题仍是最大门槛。这个S1实例锁定Python 3.9 Django 4.2 LTS。为什么不是最新版Django 5.0因为4.2是长期支持版文档最全第三方包兼容性最好且4.2的path()路由语法比老版url()更易读。Python选3.9而非3.12是因为3.9是第一个全面支持typing模块的稳定版models.py里写name: str models.CharField(max_length100)时类型提示不会报错而3.12某些教育机构的旧服务器还没编译好。安装命令就一行pip install Django4.2,4.3加引号防shell把逗号当命令分隔符。虚拟环境必须用venv而非conda——前者是Python原生方案python -m venv venv创建source venv/bin/activate激活没有conda的channel源切换焦虑。我要求实习生第一件事就是删掉全局Python环境所有项目隔离运行。曾有个学生全局装了pandas结果Django的manage.py启动时报ImportError: cannot import name six查了三天才发现是pandas依赖的six版本和Django冲突。环境隔离不是教条是止损策略。3. 核心模块实现与关键细节从模型定义到模板渲染的完整闭环3.1 数据模型设计如何用三张表撑起选课逻辑系统只需三张模型表Student、Course、Enrollment。重点不在数量而在关系建模的意图表达。Student模型长这样class Student(models.Model): student_id models.CharField(max_length12, uniqueTrue, verbose_name学号) name models.CharField(max_length50, verbose_name姓名) email models.EmailField(verbose_name邮箱, blankTrue) def __str__(self): return f{self.student_id} - {self.name}注意verbose_name参数——它不是可有可无的装饰。当你在Django Admin后台点进学生列表表头自动显示“学号”“姓名”“邮箱”而不是冷冰冰的student_id字段名。uniqueTrue强制学号唯一避免数据污染。Enrollment是核心关联表class Enrollment(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, verbose_name学生) course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name课程) enrolled_at models.DateTimeField(auto_now_addTrue, verbose_name选课时间) class Meta: unique_together (student, course) # 防止同一学生重复选同一门课 verbose_name 选课记录 verbose_name_plural 选课记录on_deletemodels.CASCADE意味着删课程时自动清空所有选课记录符合业务逻辑unique_together是数据库级约束比在视图里写if Enrollment.objects.filter(students, coursec).exists():更可靠。这里埋了个坑如果漏写unique_together学生点两次“选课”按钮就会生成两条记录后续统计人数时出错。我让学生自己故意删掉这行然后用python manage.py shell手动创建重复记录再跑Course.objects.annotate(enroll_countCount(enrollment)).values(name, enroll_count)结果发现某门课显示选了200人实际只有100个学生——这就是没理解unique_together物理意义的代价。3.2 视图逻辑函数式视图比类视图更适合初学者热搜词里有“django创建app”“django重定向传递数据”说明路由和跳转是高频痛点。这个实例全部用函数式视图FBV拒绝ListView/DetailView等类视图。为什么类视图把URL参数解析、查询集获取、模板渲染打包成黑盒新手调get_context_data()时不知道self.kwargs从哪来。函数式视图则像流水线request.GET.get(q)取搜索关键词Course.objects.filter(name__icontainsq)写查询render(request, courses.html, {courses: courses})传数据。看选课动作的视图def enroll_course(request, course_id): if not request.user.is_authenticated: return redirect(login) student Student.objects.get(userrequest.user) # 假设用户已关联Student course get_object_or_404(Course, idcourse_id) # 检查是否已选 if Enrollment.objects.filter(studentstudent, coursecourse).exists(): messages.warning(request, f你已选修《{course.name}》) return redirect(course_list) # 检查课程容量 if course.enrolled_count() course.capacity: messages.error(request, f《{course.name}》已满员) return redirect(course_list) Enrollment.objects.create(studentstudent, coursecourse) messages.success(request, f成功选修《{course.name}》) return redirect(student_dashboard)这里messages模块是Django内置的闪现消息系统比自己写session存提示更安全。get_object_or_404替代Course.objects.get()避免ID不存在时抛DoesNotExist异常导致500错误。关键在enrolled_count()这个模型方法class Course(models.Model): # ... 字段定义 def enrolled_count(self): return self.enrollment_set.count()enrollment_set是Django自动生成的反向关系管理器命名规则是小写模型名_set。新手常写成Enrollment.objects.filter(courseself)多此一举。这个方法被模板直接调用{{ course.enrolled_count }}体现Django“DRY”Dont Repeat Yourself原则。3.3 模板渲染DTL语法如何替代JavaScript交互热搜词里有“css 鼠标移入事件”“html一键返回顶部算法”暗示新手总想用前端技术解决后端问题。这个实例坚持用DTL完成交互选课按钮的禁用状态由后端计算!-- course_list.html -- {% for course in courses %} div classcourse-card h3{{ course.name }}/h3 p已选 {{ course.enrolled_count }} / {{ course.capacity }}/p {% if course.enrolled_count course.capacity %} button disabled classbtn btn-disabled已满/button {% elif course.id in enrolled_course_ids %} button disabled classbtn btn-success已选/button {% else %} a href{% url enroll_course course.id %} classbtn btn-primary选课/a {% endif %} /div {% endfor %}enrolled_course_ids是视图里传来的列表enrolled_course_ids list(Enrollment.objects.filter(studentstudent).values_list(course_id, flatTrue))。这样按钮状态完全由后端数据决定无需AJAX请求也不用写document.getElementById().disabled true。CSS“删除线”用text-decoration: line-through实现已选课程名比用JS toggle class更稳定。整个页面没有一行JavaScript但功能完整——这正是Django“服务端渲染”的初心把复杂逻辑留在可控的Python环境里。4. 实操全流程与避坑指南从创建项目到本地部署的每一步4.1 创建项目与App的标准化流程按热搜词“django创建app”很多人卡在python manage.py startapp之后。标准流程必须包含四步验证创建项目django-admin startproject school_system注意django-admin是全局命令不是python manage.py。新手常输成python manage.py startproject报错No module named django.core.management因为此时还没进入项目目录。创建Appcd school_system python manage.py startapp studentsApp名用复数students而非单数student因为Django约定App管理一类资源models.py里定义Student模型views.py里写student_list视图语义统一。注册App在school_system/settings.py的INSTALLED_APPS里添加students必须加引号且逗号不能漏。曾有个学生漏了逗号INSTALLED_APPS [django.contrib.admin, students]变成元组导致python manage.py migrate报TypeError: tuple object is not callable。迁移数据库python manage.py makemigrations python manage.py migratemakemigrations生成.py文件migrate执行SQL。如果中途改了模型必须先makemigrations再migrate不能跳过。我让学生故意删掉migrations/0001_initial.py再执行makemigrations生成新文件对比两个文件差异理解Django如何追踪模型变更。4.2 URL路由配置从根URL到子App的映射逻辑热搜词“django重定向传递数据”暴露了URL设计混乱。标准做法是根URL分发App内自治。school_system/urls.py只做分发from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(students.urls)), # 所有学生相关路由交由students App处理 ]students/urls.py定义具体路径from django.urls import path from . import views urlpatterns [ path(, views.course_list, namecourse_list), # GET / → 课程列表 path(enroll/int:course_id/, views.enroll_course, nameenroll_course), # POST /enroll/1/ path(dashboard/, views.student_dashboard, namestudent_dashboard), ]namecourse_list是关键——模板里用{% url course_list %}生成URL而不是硬编码/。这样未来把课程列表路径改成/courses/只需改urls.py所有模板自动生效。int:course_id是路径参数转换器确保传入enroll_course视图的course_id一定是整数避免SQL注入。我让学生把int:course_id改成slug:course_id再访问/enroll/abc/Django直接返回404不进视图函数——这就是路由层的安全过滤。4.3 静态文件与媒体文件的物理路径管理热搜词“html➕css➕js基础语法”“css字体”指向静态资源。Django要求严格分离开发与生产环境的静态文件路径开发环境STATIC_URL /static/STATICFILES_DIRS [BASE_DIR / static]所有CSS/JS放在static/css/main.css模板里写link href{% static css/main.css %} relstylesheet。{% static %}模板标签会自动拼接STATIC_URL前缀。生产环境STATIC_ROOT BASE_DIR / staticfiles运行python manage.py collectstatic将所有App的static目录及STATICFILES_DIRS下的文件拷贝到staticfiles目录Nginx直接服务该目录。媒体文件如学生头像同理MEDIA_URL /media/MEDIA_ROOT BASE_DIR / media。settings.py末尾必须加if DEBUG: from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)否则开发时上传的图片404。我让学生上传一张10MB的头像观察MEDIA_ROOT目录下文件大小再删掉if DEBUG:那段代码刷新页面看404——用物理反馈建立对路径配置的理解。5. 常见问题排查与独家经验那些文档里不会写的实战技巧5.1 “sqlite3 no column named unnamed”错误的根因与修复这是热搜词里最刺眼的报错。它通常发生在两种场景场景一模型字段名拼写错误比如把student_id models.CharField(...)写成studentid models.CharField(...)然后执行makemigrations。Django生成的迁移文件里operations数组会包含AddField(model_namestudent, namestudentid, ...)但数据库表里实际字段是student_id旧名。当migrate执行时SQLite尝试ALTER TABLE students ADD COLUMN studentid VARCHAR(12)但表里已有student_idDjango内部映射混乱报no column named unnamed。修复删除最新生成的迁移文件如students/migrations/0002_auto_*.py修正模型字段名运行python manage.py makemigrations --empty students生成空迁移在空迁移文件的operations里手动写migrations.RenameField( model_namestudent, old_namestudentid, new_namestudent_id, ),python manage.py migrate场景二删除字段后未清除数据库残留删掉email models.EmailField()字段makemigrations生成RemoveField操作但SQLite不支持DROP COLUMNDjango会重建表。若重建失败旧字段残留导致冲突。终极方案删db.sqlite3重跑migrate。教学时我允许学生这么做因为S1实例数据可重置。5.2 模板继承中的“块覆盖失效”问题新手常抱怨{% block content %}在子模板里不生效。典型错误是父模板base.html里写了{% block content %}{% endblock %}但子模板course_list.html里写成{% block main %}{% endblock %}。Django按块名匹配名不匹配则忽略。另一个坑是{% extends base.html %}没写在文件第一行前面有空格或注释Django解析失败。调试技巧在base.html的{% block content %}里加DEBUG: {{ block.super }}如果子模板没正确继承这里会输出空字符串如果继承了但块内容为空会显示DEBUG:。我让学生在base.html里加h1Base Template Loaded/h1在子模板{% block content %}里加h2Child Content/h2刷新页面看是否同时出现——这是最直观的继承验证法。5.3 Admin界面美化不用第三方包的三行CSS方案热搜词“django admin界面美化”催生一堆插件但S1实例用原生方案在students/static/admin/css/custom.css里写/* 让Admin列表页课程名称加粗 */ .field-name { font-weight: bold; } /* 选课记录页时间列右对齐 */ .field-enrolled_at { text-align: right; } /* 管理员登录页背景变浅灰 */ body.login { background-color: #f5f5f5; }然后在students/admin.py里注册from django.contrib import admin from .models import Student, Course, Enrollment class StudentAdmin(admin.ModelAdmin): class Media: css { all: (admin/css/custom.css,) } admin.site.register(Student, StudentAdmin)class Media是Django Admin的静态资源注入点比全局覆盖/static/admin/css/base.css更安全。我让学生修改font-weight: bold为font-size: 24px看Admin列表文字瞬间变大——用即时反馈建立CSS与Admin的关联感。5.4 本地部署的最小化Nginx配置虽然S1实例用runserver开发但部署时需Nginx。最小配置如下/etc/nginx/sites-available/schoolupstream django_app { server 127.0.0.1:8000; # Gunicorn监听地址 } server { listen 80; server_name localhost; location /static/ { alias /path/to/school_system/staticfiles/; # collectstatic目标目录 } location /media/ { alias /path/to/school_system/media/; # MEDIA_ROOT目录 } location / { proxy_pass http://django_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }关键点location /static/末尾的斜杠不能少否则/static/css/main.css会映射到/path/to/.../staticfiles/css/main.cssproxy_pass后不加URI保证路径透传。我让学生用curl -I http://localhost/static/css/main.css检查HTTP状态码200表示静态文件服务成功404则检查alias路径是否拼错。6. 后续演进路径从S1到生产级系统的自然生长这个S1实例不是终点而是生长点。我带过的团队用它延伸出三个方向方向一数据可视化增强在student_dashboard.html里嵌入Chart.js用Enrollment.objects.values(course__name).annotate(countCount(id))聚合数据生成柱状图。不碰ECharts的复杂配置就用CDN引入script srchttps://cdn.jsdelivr.net/npm/chart.js/script一行JS初始化图表。这是从“能用”到“好用”的跨越。方向二权限精细化把is_authenticated检查升级为Django Groups创建“学生”“教师”“管理员”组user_passes_test(lambda u: u.groups.filter(name教师).exists())装饰视图。比硬编码if request.user.username.startswith(T):更可持续。方向三API化改造用Django REST Framework重写视图serializers.py定义数据格式urls.py加path(api/courses/, CourseListAPIView.as_view())。前端用Fetch API调用为未来接入小程序打基础。但所有演进都遵循一个铁律每次只加一个新概念。加Chart.js时不碰权限加DRF时不改数据库结构。就像搭乐高S1是底座每一块新积木都严丝合缝扣在已有结构上。我最后给学生的建议是把这个系统部署到免费云服务器如Oracle Cloud的永远免费套餐用真实域名访问让家人同学去点“选课”按钮——当看到messages.success弹出“成功选修《Python编程》”时那种真实的反馈比任何教程都深刻。
返回列表