简介:这是一个基于Python Django与Vue.js前后端分离的学生信息管理系统毕业设计项目,适合计算机相关专业学生用于课程设计、毕业设计或项目实战练手。系统采用B/S架构,后端以Python实现接口,前端以Vue.js构建页面,功能覆盖学生管理、班级管理、用户管理、日志管理及系统信息模块,并内置演示账号(admin123),便于快速体验完整流程。资源包共391个文件,以Python源码(36个py)、Vue组件(15个vue)、TypeScript逻辑(34个ts)、JavaScript脚本(10个js)及图片资源等组成,压缩包大小20.62MB,目录结构清晰,server与web分置,方便定位前后端代码。目前已有1612人学习,压缩包内还附带部署运行说明,可帮助使用者从环境搭建到功能验证逐步完成,尤其适合需要一份可运行、易扩展的中小型管理系统源码作为参考的开发者。
1. 学生信息管理系统用 Django+Vue 前后端分离,课程设计到底在做什么
毕业设计或课程设计做到“学生信息管理系统”这个题目时,最常见的坑不是写不出代码,而是不知道前后端分离这条线要从哪里断开再拼起来。Django 管数据模型和 REST 接口,Vue 管页面交互和状态,两边通过 JSON 通信——听起来清晰,但真正做起来,模型字段怎么定、接口按什么风格暴露、CORS 配置改哪里、前端请求怎么带 token,每一步都可能卡住一整晚。这篇按工程顺序把整条链路讲完:从建 Django 工程、建表、出接口,到搭 Vue 页面、封装 Axios,再到联调部署,最后落到 admin 优化和排错技巧。适合正在做学生信息系统课设或毕设的人,也给 Django 已能写 Model、Vue 刚看懂组件的读者补一块拼图。
2. Django 后端:从数据模型到 REST 接口的落地写法
学生信息管理系统的核心业务就是三个对象:学生、课程、成绩。Django 侧先把这层关系建模清楚,后面写接口和前端页面都顺。常见访问模式是:管理员登录 → 维护学生信息 → 维护课程 → 录入成绩 → 按学生或课程查成绩。
2.1 建项目与 App 的命令行顺序
拿一份干净环境开始。Python 版本建议 3.10 或 3.11,Django 用 4.x 或 5.x,djangorestframework 和 django-cors-headers 一起装上。避免全局环境混乱,先建虚拟环境再装依赖。
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django djangorestframework django-cors-headers django-admin startproject sis cd sis python manage.py startapp students python manage.py migrate python manage.py runserver命令顺序不能乱。先建虚拟环境再装包,避免把依赖装进全局 Python;startproject sis生成的是配置目录,startapp students才是业务代码所在的地方。这里把整个业务塞进一个studentsapp 在课程设计里完全够用,不需要按模块拆多个 app。启动后用浏览器打开 Django 自带的欢迎页,确认基础工程没问题。
接下来在settings.py里注册 app,并把 DRF 和 corsheaders 加进INSTALLED_APPS:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', 'students', ]顺序有讲究:corsheaders要放在django.middleware.SecurityMiddleware之后、CommonMiddleware之前,否则中间件顺序不对时跨域头不生效;业务 app 放最后是为了避免覆盖内置模板或迁移目录。
2.2 数据模型设计:学生、课程、成绩三张表
学生信息系统的表结构并不复杂,但字段选型直接决定接口返回的 JSON 长什么样。下面是一版可以用的模型定义:
from django.db import models class Student(models.Model): student_no = models.CharField(max_length=20, unique=True, verbose_name="学号") name = models.CharField(max_length=50, verbose_name="姓名") gender = models.CharField( max_length=4, choices=(("M", "男"), ("F", "女")), verbose_name="性别" ) enroll_year = models.IntegerField(verbose_name="入学年份") def __str__(self): return f"{self.student_no} {self.name}" class Course(models.Model): code = models.CharField(max_length=20, unique=True, verbose_name="课程代码") name = models.CharField(max_length=100, verbose_name="课程名称") credit = models.DecimalField(max_digits=3, decimal_places=1, verbose_name="学分") def __str__(self): return self.name class Score(models.Model): student = models.ForeignKey( Student, on_delete=models.CASCADE, related_name="scores", verbose_name="学生" ) course = models.ForeignKey( Course, on_delete=models.CASCADE, related_name="scores", verbose_name="课程" ) score = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="分数") class Meta: unique_together = ("student", "course")几个关键决策说清楚。unique_together保证同一个学生同一门课只有一条成绩记录,这是成绩表最核心的约束,不加的话前端提交两次就会产生重复数据。on_delete=models.CASCADE表示删除学生时级联删除该生成绩,教务场景里这个行为是合理的。related_name="scores"让查询可以写成student.scores.all(),反向查询比默认的score_set可读性好得多。分数用DecimalField而不是FloatField,因为浮点数在 JSON 序列化和前端展示时可能出现 89.9999 这种误差,Decimal 序列化出来是字符串,精度可控。
建好模型后执行迁移,不需要手写 SQL:
python manage.py makemigrations students python manage.py migrate2.3 用 DRF 序列化器与视图函数出接口
接口风格按资源划分,学生、课程、成绩各是一组 REST 接口。先约定好 URL 设计,前端照着写请求就不会乱:
| 资源 | 方法 | URL 模式 | 说明 |
|---|---|---|---|
| 学生列表 | GET | /api/students/ | 返回分页后的学生列表 |
| 创建学生 | POST | /api/students/ | 提交 JSON 表单数据 |
| 单个学生 | GET / PUT / DELETE | /api/students/{id}/ | 详情、更新、删除 |
| 课程列表 | GET / POST | /api/courses/ | 课程管理 |
| 成绩列表 | GET / POST | /api/scores/ | 成绩录入与查询 |
序列化器用 ModelSerializer 最省事,它能根据模型字段自动生成校验规则。视图层选 ModelViewSet,它把 list、create、retrieve、update、partial_update、destroy 六个动作一次性配齐,课程设计阶段不需要过度设计。
from rest_framework import serializers, viewsets from rest_framework.routers import DefaultRouter from .models import Student, Course, Score class ScoreSerializer(serializers.ModelSerializer): class Meta: model = Score fields = ["id", "student", "course", "score"] class StudentSerializer(serializers.ModelSerializer): class Meta: model = Student fields = ["id", "student_no", "name", "gender", "enroll_year"] class StudentViewSet(viewsets.ModelViewSet): queryset = Student.objects.all().order_by("student_no") serializer_class = StudentSerializer class CourseViewSet(viewsets.ModelViewSet): queryset = Course.objects.all().order_by("code") serializer_class = serializers.ModelSerializer class ScoreViewSet(viewsets.ModelViewSet): queryset = Score.objects.all() serializer_class = ScoreSerializer router = DefaultRouter() router.register("students", StudentViewSet) router.register("courses", CourseViewSet) router.register("scores", ScoreViewSet)视图里的queryset是 DRF 取数的唯一来源,必须显式写order_by,否则分页时多次请求返回顺序不稳定。DefaultRouter会自动生成带尾部斜杠的路由,对应表格里的 URL 模式。最后在项目的urls.py里把router.urls挂到api/前缀下:
urlpatterns = [ path("admin/", admin.site.urls), path("api/", include(router.urls)), ]请求/api/students/时返回的 JSON 会包含分页信息,结构和前端要消费的数据格式强相关,下一节细说。
2.4 过滤、分页与搜索的默认配置
如果不配分页,全年级学生一次性塞进一个 JSON 响应里,到几百上千条时前端表格会明显卡顿。DRF 的分页默认是关闭的,需要显式打开。在settings.py里加一段:
REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 20, "DEFAULT_FILTER_BACKENDS": [ "django_filters.rest_framework.DjangoFilterBackend", "rest_framework.filters.SearchFilter", ], }这里引入了django_filters,需要单独安装:pip install django-filter。然后在视图里声明哪些字段可过滤、哪些字段可搜索:
class StudentViewSet(viewsets.ModelViewSet): queryset = Student.objects.all().order_by("student_no") serializer_class = StudentSerializer filterset_fields = ["gender", "enroll_year"] search_fields = ["student_no", "name"]配置生效后,URL 上的查询参数直接控制返回结果:
| 请求参数 | 含义 | 示例 |
|---|---|---|
| page | 页码,从 1 开始 | /api/students/?page=2 |
| gender | 精确过滤字段 | /api/students/?gender=M |
| enroll_year | 入学年份精确过滤 | /api/students/?enroll_year=2023 |
| search | 模糊搜索 | /api/students/?search=张 |
接口返回的标准结构是{"count": 总条数, "next": "下一页URL", "previous": "上一页URL", "results": [当前页数据]}。前端的分页组件依赖count和results这两个字段,这个结构 Django 不会自动帮你写进前端,需要自己记住。如果想让接口支持page_size参数动态调整每页条数,可以重写get_paginated_response,但课程设计阶段固定 20 条每页足够。
3. Vue 前端:组件、路由和 Axios 封装的拆分思路
前端侧的任务是把后端接口变成可操作的页面。学生信息管理系统通常需要:登录页、学生管理页、课程管理页、成绩录入页。页面之间用 vue-router 切换,请求统一走封装好的 Axios 实例,组件用 Element Plus 快速搭建表格和表单。
3.1 用 Vite 创建 Vue 项目和依赖安装
推荐 Vite 而不是 Vue CLI,因为 Vite 创建的项目更轻、启动更快,也更容易理解构建配置。创建项目时直接指定模板,一步到位:
npm create vite@latest sis-web -- --template vue cd sis-web npm install npm install vue-router axios element-plus依赖分两类:vue-router管路由,axios管 HTTP 请求,element-plus提供表格、分页、表单、弹窗等现成组件。装完依赖后清理默认模板文件,把App.vue改成最简结构,只保留一个路由出口:
<template> <router-view /> </template>main.js里注册路由、Element Plus 和图标库:
import { createApp } from "vue" import App from "./App.vue" import router from "./router" import ElementPlus from "element-plus" import "element-plus/dist/index.css" createApp(App).use(router).use(ElementPlus).mount("#app")这里将 Element Plus 全量引入,好处是开发时不需要按需导入组件,缺点打包体积略大。课程设计不追求极致性能,全量引入减少踩坑面。
3.2 路由与页面懒加载
路由配置决定页面的访问路径。学生信息管理系统的页面层级很清晰:登录页独立,管理页面共用一个带侧边栏的布局。
// src/router/index.js import { createRouter, createWebHistory } from "vue-router" const router = createRouter({ history: createWebHistory(), routes: [ { path: "/", redirect: "/students" }, { path: "/login", component: () => import("../views/Login.vue") }, { path: "/", component: () => import("../layout/MainLayout.vue"), children: [ { path: "students", component: () => import("../views/student/StudentList.vue"), }, { path: "courses", component: () => import("../views/course/CourseList.vue"), }, { path: "scores", component: () => import("../views/score/ScoreList.vue"), }, ], }, ], }) export default router路由懒加载用() => import(...)实现,每个页面会被拆成独立的 JavaScript chunk,用户访问哪个页面才加载哪个页面。首屏只拉登录页代码,加载速度比一次性打包全部页面快。子路由共用MainLayout.vue布局组件,布局内部只需放一个<router-view />。
3.3 Axios 封装:token 注入与 401 处理
前后端分离最容易出问题的环节是请求认证。Django 默认的 session 认证在跨域场景下不好用,常见做法是后端配 SimpleJWT,前端在登录成功后把 token 存到 localStorage。Axios 封装的核心就两件事:请求时带上 token,响应遇到 401 时踢回登录页。
| 功能 | 实现方式 | 位置 |
|---|---|---|
| 统一 baseURL | axios.create({ baseURL: "/api" }) | src/utils/request.js |
| 注入 token | 请求拦截器读 localStorage | 同上 |
| 401 跳转 | 响应拦截器判断 status | 同上 |
| 错误提示 | ElMessage.error 弹出 | 同上 |
立刻写一份能直接用的封装:
// src/utils/request.js import axios from "axios" import { ElMessage } from "element-plus" const request = axios.create({ baseURL: "/api", timeout: 10000, }) request.interceptors.request.use( (config) => { const token = localStorage.getItem("token") if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => Promise.reject(error) ) request.interceptors.response.use( (response) => response.data, (error) => { if (error.response && error.response.status === 401) { localStorage.removeItem("token") location.href = "/login" } else { ElMessage.error(error.response?.data?.detail || "请求失败") } return Promise.reject(error) } ) export default requestbaseURL写成/api而不是http://127.0.0.1:8000/api,这是前后端联调的关键约定。开发时 Vite 负责把/api开头的请求转发到 Django,生产环境由 Nginx 负责,前端代码里不需要写死后端地址。响应拦截器直接返回response.data,业务代码里拿到的是 DRF 返回的 JSON 对象,不用每处都写.data。
3.4 学生列表页的完整实现
学生列表页是前端工作量最大的部分,包含搜索表单、数据表格、分页三块。用 Element Plus 写起来很直接:
<template> <div> <el-form :inline="true" @submit.prevent="handleSearch"> <el-form-item label="姓名"> <el-input v-model="query.search" placeholder="输入姓名或学号" clearable /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> </el-form-item> </el-form> <el-table :data="students" v-loading="loading" border> <el-table-column prop="student_no" label="学号" width="140" /> <el-table-column prop="name" label="姓名" width="120" /> <el-table-column prop="enroll_year" label="入学年份" width="120" /> </el-table> <el-pagination :current-page="query.page" :page-size="query.page_size" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @current-change="handlePageChange" @size-change="handleSizeChange" /> </div> </template> <script setup> import { ref, onMounted } from "vue" import request from "../../utils/request" const students = ref([]) const loading = ref(false) const total = ref(0) const query = ref({ page: 1, page_size: 20, search: "" }) async function fetchStudents() { loading.value = true try { const data = await request.get("/students/", { params: query.value }) students.value = data.results total.value = data.count } finally { loading.value = false } } function handleSearch() { query.value.page = 1 fetchStudents() } function handlePageChange(page) { query.value.page = page fetchStudents() } function handleSizeChange(size) { query.value.page_size = size query.value.page = 1 fetchStudents() } onMounted(fetchStudents) </script>data.results和data.count就是 2.4 节里 DRF 分页返回的标准结构,这里必须对应上,字段名不同就会拿到空数据。查询时要手动把page重置为 1,因为搜索条件变化后当前页码可能超出总页数,不重置会拿到空列表。handleSizeChange里同时改page_size和page,逻辑上一处都不能少。
4. 前后端联调与部署:CORS、转发和 Nginx 配置
前端页面写好后进入联调阶段。这个阶段 80% 的问题是接口请求发不出去或响应被浏览器拦截,背后的原因集中在 CORS 和请求地址上。把开发转发和生产配置理顺,联调基本不会卡太久。
4.1 用 django-cors-headers 解决开发环境跨域
Vue 开发服务器跑在 5173 端口,Django 跑在 8000 端口,前端页面直接向后端地址发请求属于跨域。浏览器会拦截响应,控制台报 CORS error。开发环境最简单且课程设计通用的解法是 django-cors-headers。
先在settings.py里把中间件加到最前面:
MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://127.0.0.1:5173", ]中间件顺序由 Django 请求处理流程决定,CorsMiddleware 必须放在能响应请求的中间件之前,否则跨域头添加的时机太晚,浏览器已经拒绝响应。课程设计阶段只放开localhost:5173一个来源,不要用CORS_ALLOW_ALL_ORIGINS = True,那相当于所有网站都能跨域调用你的接口,答辩时被问到安全策略答不上来。
4.2 Vite 开发转发配置:前端写 /api 就够了
其实开发时不经 CORS 也能联调。Vite 开发服务器自带转发能力,把前端发往/api的请求直接送到 Django,浏览器看到的请求和响应都来自同一个 5173 端口,天然没有跨域问题。
// vite.config.js import { defineConfig } from "vite" import vue from "@vitejs/plugin-vue" export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { "/api": { target: "http://127.0.0.1:8000", changeOrigin: true, }, }, }, })转发配置生效后,前端request.js里的baseURL: "/api"就是可用的。请求时序是:浏览器 → 5173 端口 Vite 服务器 → 转发到 8000 端口 Django → 响应原路返回。这个方案同时解决了 CORS 和请求地址写死两个问题,所以我在第 3 章里刻意让 Axios 的 baseURL 只写/api。
注意,vite.config.js修改后要重启 Vite 开发服务器才生效。如果改了配置但请求还是 404,先看终端里有没有出现“proxy”相关的启动日志,没有就重启试试。
4.3 Django 生产环境配置:DEBUG 关掉后静态文件怎么办
开发时 Django 自己托管静态文件,前端也能通过 Vite 访问接口。但部署到服务器后,DEBUG = False时 Django 不会再帮你处理静态文件,必须要做两件事:指定静态文件收集目录,然后让 Nginx 直接托管它们。
# settings.py 生产环境片段 DEBUG = False ALLOWED_HOSTS = ["sis.example.com", "localhost"] STATIC_ROOT = BASE_DIR / "staticfiles"然后在项目根目录执行:
python manage.py collectstatic --noinputcollectstatic会把所有 app 下和 admin 的静态文件统一复制到staticfiles目录。文件收集完之后,Nginx 负责对/static/路径的请求返回这个目录的文件,Django 只处理/api/的请求。这样分工后 Django 不再直接面对静态资源请求,性能和安全性都更好。
4.4 前后端环境配置对照表与 Nginx 部署
部署时最容易搞混的是环境参数。列一张对照表,按表检查配置能少走弯路:
| 配置项 | 开发环境 | 生产环境 |
|---|---|---|
| Django 的 DEBUG | True | False |
| Django 的 ALLOWED_HOSTS | localhost, 127.0.0.1 | 服务器域名或 IP |
| 前端 baseURL | /api | /api |
| 静态文件服务 | Django 自带 / Vite 插件 | Nginx/static/ |
| API 转发 | Vite server.proxy | Nginx/api/转发 |
Nginx 站点配置可以这样写,root指向 Vue 打包后的 dist 目录:
server { listen 80; server_name sis.example.com; root /var/www/sis-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /static/ { alias /var/www/sis-web/staticfiles/; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html是前后端分离部署的关键行。Vue 路由是前端路由,刷新/students这个地址时服务器没有对应文件,如果不回退到index.html就会 404。有了这行,浏览器加载index.html后 Vue Router 会接管 URL 并渲染对应页面。
前端上线前先执行构建:
npm run build构建产物在dist目录,把这个目录下所有文件拷贝到root指定的路径即可。常见错误是把dist目录本身放到 root 下,那样页面路径会多一层,刷新就白屏,检查index.html是否直接在 root 指定的第一层。
5. Django admin 界面美化与调试提速的三个技巧
最后一章写三个能立刻用上的落地技巧,让课设或毕设不只停留在“能跑”的程度。
5.1 admin 后台配出可读的表单和列表
Django 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", "enroll_year") search_fields = ("student_no", "name") list_filter = ("gender", "enroll_year") @admin.register(Score) class ScoreAdmin(admin.ModelAdmin): list_display = ("student", "course", "score") search_fields = ("student__name", "course__name")list_display控制列表展示哪些列,search_fields生成搜索框,list_filter在右侧生成筛选面板。关联字段搜索写成student__name这种双下划线写法,Django 会自动 JOIN 查询。这些配置不止是界面问题,它直接影响你录入测试数据的效率——测试数据录得顺,接口和前端验证才会快。
5.2 联调时先看两个地方
接口返回不对时,第一反应不要改代码。先打开浏览器开发者工具的 Network 面板,看请求 URL、状态码和响应体。前后端分离项目 90% 的问题在这里能看出来:请求显示 404 是路由或 URL 拼写问题,显示 500 是 Django 视图或模型报错,显示 200 但数据不对是序列化器或前端字段名不匹配。
后端出错时看 Django 运行日志。开发服务器会在终端直接打印 Traceback,里面会写清楚哪个文件哪一行报错。django的日志定位比前端更快,因为 Python 的 Traceback 直接指向代码行。顺手再确认浏览器请求的 Content-Type 是不是application/json,前端 POST 表单没设置这个头也会导致后端取不到数据。
5.3 用表驱动测试数据生成
接口调试需要大量数据的时候,一条一条录是浪费时间。Django 的 shell 可以配合列表推导式一次性批量造数据:
python manage.py shellfrom students.models import Student, Course, Score import random Student.objects.bulk_create([ Student( student_no=f"2025{i:04d}", name=f"学生{i}", gender=random.choice(["M", "F"]), enroll_year=random.choice([2023, 2024, 2025]) ) for i in range(1, 51) ])bulk_create是 ORM 里批量插入的常用方式,一条 SQL 语句插入多行数据,比单条循环create快一个数量级。造完数据后可以直接用Student.objects.count()验证数量,再去前端页面翻页看分页效果。这个技巧对验证 2.4 节里配置的过滤、分页、搜索三个功能特别有效,五十条数据足够看出分页行为是否正确。
到这里,从 Django 模型到 Vue 页面再到 Nginx 部署的整条链路已经走通,剩下的工作在浏览器里逐页点一遍功能,按 5.2 节的方法排查掉最后一两个请求错误,这个项目就可以进入写文档和准备答辩的阶段了。
本文还有配套的精品资源,点击获取