ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Django学生成绩管理系统:模型约束、查询优化与layuimini前端集成

2026/9/16 3:56:16 拓冰建站 浏览量
Django学生成绩管理系统:模型约束、查询优化与layuimini前端集成 简介这是一套基于Django的学生成绩管理系统完整源码与配套数据库面向计算机、通信、人工智能等专业的高校学生和老师用于快速搭建一个可运行的成绩管理项目也适用于期末课程设计、大作业或毕业设计参考。系统采用Python/Django框架前端整合layui、Font Awesome等成熟组件界面交互较完整代码经过调试可正常启动支持在原有基础上二次开发扩展功能。压缩包共362个文件体积约2.96MB文件类型涵盖Python源码、HTML模板、CSS/SCSS/LESS样式、JavaScript脚本、图片与字体资源、JSON配置、SQL数据库脚本及Excel数据等既有核心业务逻辑也有可直接导入的数据库结构与示例数据。目前已有120人学习。这套资料曾获98分答辩评审成绩项目整体结构清晰对希望理解Django项目分层、成绩管理流程或快速获取高分毕设模板的读者具有较高借鉴价值。1. 为什么这套学生成绩管理系统值得拆着看毕业季看到“Django 学生成绩管理系统”这个关键词多数人第一反应是又一份模板级 CRUD。但我把这份源码的静态资源清单扫了一遍发现里面不是默认的 Django admin 皮肤而是 layuimini 后台框架、wangEditor 富文本、zyupload 上传组件成套出现——说明作者是在认真做界面和交互而不是交一份能跑就行的课程作业。答辩评审给到 98 分靠的不是功能堆砌而是模型设计、权限边界和查询细节都有完整闭环。适合两类人一是要用 Django 做课程设计或毕设想找一份不是“玩具项目”的参照系二是已经在写业务系统想看看成绩、选课这类经典场景里的表结构设计和查询优化该怎么落。接下来按“结构 → 模型 → 视图 → 前端接入 → 部署验证”的顺序拆代码可以直接抄。2. 项目结构与数据模型从目录到成绩表的唯一约束2.1 目录结构与虚拟环境先分清源码包和运行环境解压后第一眼不要急着runserver先看顶层文件。项目自带pyvenv.cfg说明作者在打包时直接把虚拟环境文件夹一起导出了但换一台机器这个虚拟环境未必能用因为里面记录的home路径是原机器的 Python 安装目录。常见做法是删掉这个 venv 目录用你本机的 Python 重新建环境python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt如果依赖文件里锁定的是Django3.x这类版本直接装即可如果只写了Django没锁版本我一般会先装 Django 3.2 LTS因为 4.x 之后对 MySQL 的适配和部分第三方组件兼容性需要额外处理。静态资源集中在static目录下layui.css、font-awesome.css、layer.css是后台 UI 的基础wangEditor负责富文本zyupload负责文件上传后面第四章会讲它们怎么和 Django 模板协作。先确认项目根目录下有没有manage.py有就说明是标准 Django 工程结构可以直接按 1.1 的方式拉起。2.2 核心模型学生、课程、成绩的关系与约束成绩管理系统的表设计不难但很容易做坏。最常见的翻车写法是把所有成绩字段塞进一张学生表score_math、score_english、score_python……一旦要加课程就得改表结构。这套项目用的是标准三表设计学生表、课程表、成绩表成绩表通过外键关联学生和课程这是我在生产环境里也会采用的方案。核心模型大致如下from django.db import models class Student(models.Model): student_no models.CharField(学号, max_length20, uniqueTrue) name models.CharField(姓名, max_length50) gender models.CharField(性别, max_length2, choices[(男, 男), (女, 女)]) class_name models.CharField(班级, max_length50, db_indexTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) def __str__(self): return f{self.student_no} - {self.name} class Course(models.Model): course_no models.CharField(课程编号, max_length20, uniqueTrue) course_name models.CharField(课程名称, max_length100) credit models.DecimalField(学分, max_digits2, decimal_places1) teacher models.CharField(任课教师, max_length50, blankTrue) def __str__(self): return self.course_name class Score(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, verbose_name学生) course models.ForeignKey(Course, on_deletemodels.CASCADE, verbose_name课程) score models.DecimalField(成绩, max_digits5, decimal_places1) exam_date models.DateField(考试日期) class Meta: verbose_name 成绩 constraints [ models.UniqueConstraint(fields[student, course], nameunique_student_course) ] def __str__(self): return f{self.student} | {self.course} | {self.score}简单说明三个设计点。第一Score表里用UniqueConstraint(fields[student, course])做联合唯一约束这是防止同一门课成绩重复录入的关键比在视图层写if Score.objects.filter(...).exists()更可靠因为数据库层面的约束在任何并发入口下都有效。第二Score.score用的是DecimalField而不是FloatField因为浮点数在 Python 里对 89.5、90.3 这类成绩做求和、平均时会产生二进制精度误差比如 89.5 可能显示成 89.499999。DecimalField配合decimal_places1能保证聚合运算结果准确。第三student_no和course_no都加了uniqueTrue很多毕设在录入时不校验学号重复会导致一个学生被创建两次后面统计成绩时出现“幽灵数据”。加了唯一约束后重复写入会直接抛IntegrityError通过表单层捕获即可。2.2.1 为什么成绩表不用宽表部分从 Excel 思维出发的同学会把成绩表设计成student_id, java_score, mysql_score, python_score这种宽表这种设计的增删课程必须迁移表结构而且查询“某学生所有成绩”要拼十几个字段的空值判断。三表设计的好处是课程数据可以动态增删成绩统计时只需要按student聚合。这套项目能支撑多学期、多课程扩展功劳全在Score表的松耦合设计上。在settings.py里数据库配置默认可以切到 MySQL但本地学习阶段用 SQLite 更省事DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } # MySQL 环境参考配置 # ENGINE: django.db.backends.mysql, # NAME: score_system, # USER: root, # PASSWORD: your_password, # HOST: 127.0.0.1, # PORT: 3306, }SQLite 对毕设和中小型管理系统的并发量完全够用而且没有mysqlclient编译依赖。等部署到服务器要换 MySQL 时只需要改掉这段配置并迁移数据业务代码不用动。3. 查询、录入与防重把成绩管理的核心流程写扎实3.1 录入接口与唯一约束防止同一条成绩被重复提交成绩录入是这类系统的核心操作最容易出问题的不是保存而是“改一次成绩变成了插入两条”。“录入页面提交表单 → 后端保存”这段逻辑我建议这样做表单里带上score_id有值就是更新没值就是新增。配合模型层的UniqueConstraint即便用户开了两个标签页同时录也只会有一条生效。from django.shortcuts import render, redirect, get_object_or_404 from django.db import IntegrityError from .models import Score, Student, Course from .forms import ScoreForm def score_save(request): if request.method POST: form ScoreForm(request.POST) if form.is_valid(): score_id request.POST.get(score_id) if score_id: score_obj get_object_or_404(Score, pkscore_id) # 更新场景保留 student / course 外键只改分数和考试日期 score_obj.score form.cleaned_data[score] score_obj.exam_date form.cleaned_data[exam_date] score_obj.save() return redirect(score_list) try: form.save() return redirect(score_list) except IntegrityError: # 联合唯一约束触发时给出可读提示 return render(request, score/form.html, {form: form, error: 该学生此课程成绩已存在}) else: form ScoreForm() return render(request, score/form.html, {form: form})逻辑说明表单校验通过后先判断是编辑还是新增新增分支里form.save()如果抛IntegrityError说明数据库层面的联合唯一约束拦住了重复数据此时把错误信息渲染回模板。更新分支不需要走唯一约束检查因为学号、课程号没有变化。表单类里有个细节容易被忽略Score的外键字段在模型上允许空白但表单显示时默认是ModelChoiceField下拉框需要手动指定queryset。如果是想按班级先过滤学生列表可以在__init__里根据请求参数动态裁剪queryset减少下拉框里的数据量。3.2 成绩检索视图用 Q 对象处理多条件组合查询学生成绩系统的检索需求通常是教师输入一个关键字同时匹配学号、姓名、班级再加一个课程筛选条件。如果连写多个filter当某个字段为空时就会误过滤掉结果。项目里用的是Q对象动态拼接这是 Django 查询里所有组合检索的通用解法from django.db.models import Q def score_list(request): keyword request.GET.get(keyword, ).strip() course_id request.GET.get(course_id, ).strip() scores Score.objects.select_related(student, course).all() if keyword: scores scores.filter( Q(student__student_no__icontainskeyword) | Q(student__name__icontainskeyword) | Q(student__class_name__icontainskeyword) ) if course_id: scores scores.filter(course_idcourse_id) # 按考试日期倒序保证最近录入的成绩排前面 scores scores.order_by(-exam_date, student__student_no) # 分页每页 20 条 paginator Paginator(scores, 20) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, score/list.html, {page_obj: page_obj, keyword: keyword})参数说明student__student_no__icontains表示跨外键查询学生表的学号字段icontains是不区分大小写的模糊匹配对中文和英文都适用select_related把学生和课程两个外键通过一次LEFT JOIN带出来避免循环访问时产生 N1 次查询。成绩列表页的排序我习惯按考试日期倒序因为教师最常看的是最近一次考试。如果系统里有大量历史数据keyword不要做全表扫描可以在class_name和student_no字段上建立索引。3.3 admin 后端把模型暴露给管理界面模型的 admin 注册不要只写admin.site.register(Score)就结束那会导致后台列表页显示的是Score object (1)查找数据只能靠猜。项目定制过的 admin 配置如下这套写法也是最常见的后台列表优化方式from django.contrib import admin from .models import Student, Course, Score admin.register(Student) class StudentAdmin(admin.ModelAdmin): list_display (student_no, name, gender, class_name, created_at) search_fields (student_no, name, class_name) list_filter (class_name, gender) admin.register(Score) class ScoreAdmin(admin.ModelAdmin): list_display (student, course, score, exam_date) search_fields (student__name, student__student_no, course__course_name) list_filter (course, exam_date) list_per_page 20 admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display (course_no, course_name, credit, teacher)配置项作用适用字段list_display控制列表页显示的列模型字段或方法名search_fields生成后台搜索框支持跨外键查询student__name这种写法可跨表list_filter右侧过滤栏适合枚举字段class_name、courselist_per_page每页显示条数避免长列表卡顿任意正整数search_fields里写student__name是后台搜索跨外键的核心语法输入学生姓名就能筛选对应成绩。这段配置同时兼顾了“Django admin 界面美化”——虽然不换皮肤但信息层级清楚教师直接用 admin 就能完成大半数据维护工作。4. layuimini 后台与文件上传把 Django 服务端和前端组件接起来4.1 layuimini 静态资源与模板继承很多 Django 毕设的后台页面用的是默认模板表格样式停留在 bootstrap 2 时代。这套项目引入 layuimini 的价值在于它是一个基于 Layui 的轻量后台布局框架自带侧边栏、顶栏、标签页和 Django 模板并不冲突。静态资源里能看到layui.css、layuimini.css、font-awesome.css说明前端采用了本地静态文件方式不需要 CDN离线也能跑。接入方式不是重写 Django 的 admin 页面而是用 Django 模板继承实现独立的管理端页面。常见做法是做一个base.html放在templates/layout下{% load static %} !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 link relstylesheet href{% static layuimini/css/layuimini.css %} link relstylesheet href{% static layui/css/layui.css %} link relstylesheet href{% static font-awesome/css/font-awesome.min.css %} title{% block title %}成绩管理系统{% endblock %}/title /head body div classlayuimini-container div classlayuimini-main {% block content %}{% endblock %} /div /div script src{% static layui/layui.js %}/script {% block js %}{% endblock %} /body /html子页面只需继承并填内容块例如成绩列表页{% extends layout/base.html %} {% block content %} table classlayui-table thead tr th学号/th th姓名/th th课程/th th成绩/th /tr /thead tbody {% for score in page_obj %} tr td{{ score.student.student_no }}/td td{{ score.student.name }}/td td{{ score.course.course_name }}/td td{{ score.score }}/td /tr {% endfor %} /tbody /table {% endblock %}写 Django 模板时要注意变量查找规则score.student.student_no会先尝试属性、再尝试字典键最终对应Score.student外键对象的student_no字段不需要在视图里手动拼数据字典。框架的静态文件放在static目录后模板里用{% static %}标签引用切到生产环境后只需collectstatic就行。4.2 富文本与上传组件接入wangEditor / zyupload静态资源里有wangEditor.css和zyupload-1.0.0.min.css说明项目里有公告通知、教学资源之类需要富文本和文件上传的功能。Django 在处理文件上传时的核心是request.FILES必须要给form加上enctypemultipart/form-data否则视图里拿到的永远是空值。上传组件的后端接口可以这样写import os from django.conf import settings from django.http import JsonResponse from django.views.decorators.http import require_POST require_POST def upload_file(request): upload request.FILES.get(file) if not upload: return JsonResponse({code: 1, msg: 未接收到文件}) allowed_types [.jpg, .png, .pdf, .docx, .xlsx] ext os.path.splitext(upload.name)[1].lower() if ext not in allowed_types: return JsonResponse({code: 1, msg: f不支持的文件类型{ext}}) file_dir os.path.join(settings.MEDIA_ROOT, uploads) os.makedirs(file_dir, exist_okTrue) save_path os.path.join(file_dir, upload.name) with open(save_path, wb) as f: for chunk in upload.chunks(): f.write(chunk) file_url f{settings.MEDIA_URL}uploads/{upload.name} return JsonResponse({code: 0, url: file_url})参数说明upload.chunks()是 Django 对文件对象的迭代方法适合大文件分块写盘直接用read()会把整个文件加载进内存。MEDIA_ROOT和MEDIA_URL需要在settings.py里配好import os MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)同时项目根urls.py要加上from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 已有路由 ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)DEBUGTrue时 Django 才会自己托管媒体文件线上部署必须交给 Nginx 这类 Web 服务器否则会有性能和安全隐患。文件名校验这块建议再加一层随机前缀防止用户上传同名文件互相覆盖new_name f{uuid.uuid4().hex}_{upload.name}。4.3 前后端数据交互与 CSRF 处理前端用 layui 的table模块请求 Django 接口时容易遇到 403 错误这个错误基本都来自 CSRF 校验。Django 默认开启 CSRF 中间件普通表单需要在模板里写{% csrf_token %}但 AJAX 请求没法用这个标签。常见做法是从 cookie 里取csrftoken并加到请求头layui.use([table, jquery], function () { var table layui.table; var $ layui.jquery; // 从 cookie 中获取 CSRF Token function getCookie(name) { var cookieValue null; if (document.cookie document.cookie ! ) { var cookies document.cookie.split(;); for (var i 0; i cookies.length; i) { var cookie cookies[i].trim(); if (cookie.substring(0, name.length 1) (name )) { cookieValue decodeURIComponent(cookie.substring(name.length 1)); break; } } } return cookieValue; } $.ajaxSetup({ beforeSend: function (xhr) { xhr.setRequestHeader(X-CSRFToken, getCookie(csrftoken)); } }); table.render({ elem: #scoreTable, url: /api/scores/, cols: [[ { field: student_no, title: 学号 }, { field: name, title: 姓名 }, { field: course_name, title: 课程 }, { field: score, title: 成绩 } ]] }); });逻辑说明$.ajaxSetup里的beforeSend会在每次 AJAX 请求前自动附加X-CSRFToken请求头不再需要每个接口单独处理。Django 校验时优先读取X-CSRFToken头再做post表单字段匹配。接口返回 JSON 时要保持字段和table.render里cols的名称一致否则表格会显示空白。如果做了前后端分离用 DRFDjango REST Framework时需要在视图类上加csrf_exempt或用SessionAuthentication但那是另一套体系当前项目的模板渲染模式用上面这段就够了。5. 跑通前后台数据库迁移、部署排错与 shell 验证5.1 数据库迁移与 mysqlclient 的替代方案拿到源码后如果数据库文件没有一并提供第一步是生成迁移并初始化注意迁移要按依赖顺序执行python manage.py makemigrations python manage.py migrate python manage.py createsuperuser如果项目配置了 MySQL 但本地没装mysqlclient在 Windows 上编译经常报错error: Microsoft Visual C 14.0 is required。常见的替代方案是安装pymysql并在项目__init__.py里加入兼容声明import pymysql pymysql.install_as_MySQLdb()这样 Django 会把pymysql当作MySQLdb使用避免装编译依赖。注意pymysql对 Django 4.x 的某些版本的兼容性问题建议锁版本pymysql1.1.0以上。迁移完成后再用createsuperuser创建管理员账号登录 admin 后台或者自定义登录页。5.2 用 django shell 做数据核对与 reverse resolve 验证页面跑起来后不要只点几个按钮就说“能用”。我习惯用django shell做一次数据闭环校验检查外键关系和路由解析是否正确。下面这段可以直接复制到 shell 里执行python manage.py shellfrom django.urls import reverse from score.models import Student, Course, Score # 1. 路由反向解析是否正常对应热搜里的 reverse resolve 场景 list_url reverse(score_list) print(f成绩列表路由: {list_url}) # 2. 创建一个学生和课程验证唯一约束是否生效 stu Student.objects.create(student_no2024001, name测试学生, class_name计科1班) course Course.objects.create(course_noCS101, course_namePython程序设计) Score.objects.create(studentstu, coursecourse, score90.5, exam_date2024-06-20) try: Score.objects.create(studentstu, coursecourse, score88.0, exam_date2024-06-21) except Exception as e: print(f重复成绩已被拦截: {type(e).__name__}) # 3. 聚合查询验证成绩平均值计算是否精确 from django.db.models import Avg avg Score.objects.filter(studentstu).aggregate(avg_scoreAvg(score)) print(f平均分: {avg})这段操作覆盖了三件事reverse(score_list)验证 URL 配置有没有写错重复插入成绩确认数据库约束生效Avg聚合确认成绩字段用DecimalField之后没有精度丢失。如果你在 shell 里执行时发现reverse抛NoReverseMatch优先检查urls.py里的name参数是否和视图模板中的{% url %}标签一致。5.3 部署前的最低检查清单项目本地能跑和上线能跑是两回事尤其当你打算把毕设展示到答辩演示或者部署到服务器。建议按这个顺序排查settings.py里的DEBUG必须改成False同时把ALLOWED_HOSTS配成你的域名或 IP不能留空。静态文件统一收集到指定目录python manage.py collectstatic宝塔部署时把STATIC_ROOT指到网站目录让 Nginx 直接托管。媒体文件目录权限确认可写否则上传功能线上会挂。数据库从 SQLite 切到 MySQL 时用户表和权限表里没有特殊字符迁移前先备份db.sqlite3。最后一条建议答辩演示前把python manage.py runserver 0.0.0.0:8000换成用 gunicorn 或 uwsgi 拉起因为runserver在处理并发请求时会阻塞在线演示时多人同时访问容易卡死。如果只需要应付演示也可以直接运行项目自带的启动脚本但要提前在无痕窗口验证一遍静态文件加载是否完整因为浏览器的缓存会掩盖一部分资源引用错误。我用select_related和Q对象把多条件查询的 N1 问题压到一次 SQL用UniqueConstraint堵住重复录入再用 layuimini 和 layui table 把 admin 之外的管理界面做成一个可以被业务扩展的前端壳。这套组合下来项目在毕设层面的完成度已经超过大多数“能 CRUD 就行”的工程换一个业务场景比如固定资产管理系统或实验室设备管理系统照同样的模型和视图骨架改也能快速复现。本文还有配套的精品资源点击获取