☰
Python学校教务系统实战:从解压部署到权限控制全攻略
2026/9/30 13:31:43 网站建设 项目流程

简介:一份面向 Python Web 初学者的学校教务系统项目,围绕学生信息管理、课程安排、成绩录入与查询等常见教务场景展开,适合想通过完整项目练习 Django/Flask、数据库设计与前后端交互的开发者。压缩包共 9 个文件,体积 467KB,核心包含 1 个 Python 主程序、7 个 SQL 数据库脚本和 1 篇项目论文文档:SQL 文件用于初始化学生、课程、教师、成绩等核心表结构,论文则用于梳理性需求分析、架构设计与技术实现。目前已有 591 人学习下载,说明该资源在同类课设与练习项目中具备一定参考价值。通过实际部署和阅读源码,可掌握 Web 框架路由与视图编写、数据库 CRUD 操作、基础前端页面交互,以及从系统设计到测试的完整开发流程,对完成课程设计或积累项目经验均有帮助。

1. 学校教务系统用 Python 落地:先想清楚这个 zip 里装的是什么

收到一个「python学校教务系统.zip」,先别急着双击解压。这个压缩包通常是一个 Django 或 Flask 的完整工程,里面装着学生、课程、成绩、选课这几条核心业务线。它解决的是教务处的真实痛点:把人工维护的 Excel 流程搬到 Web 上,教师在线录成绩、学生在线查课表和选课、教务员统一管理基础数据。适合三类人——做毕设的学生、给小型院校或培训机构搭内部系统的开发、以及想拿现成代码改造成商业产品的团队。注意,这里的 zip 是压缩包概念,和 Python 内置的 zip() 函数完全是两码事,新手容易被标题带偏。解压、配环境、跑起来只是第一步,后面改业务逻辑才是真正花时间的地方。

2. 拆包前先看懂选型和数据模型:教务系统的地基长什么样

拿到压缩包先别急着跑命令,把工程结构扫一遍。常见做法的判断标准很简单:根目录有 manage.py 的就是 Django 工程,有 app.py 加 blueprints 目录的就是 Flask。requirements.txt 里如果第一个依赖是 Django,那后面所有改造都按 Django 的思路走,别硬掰成 Flask。

2.1 教务系统的核心模型:学生、课程、选课、成绩的关系别设计错

教务系统绕不开这几张表:学生、教师、课程、开课计划、选课记录、成绩。最关键的是一张带属性的中间表。很多初学的人图省事,在 Student 上直接写 ManyToManyField(Course),结果成绩字段没地方放,最后只能把成绩挂在 Course 上,同一个学生重修同一门课时成绩互相覆盖,这就是典型的翻车设计。

正确做法是把「选课」单独建成一张表,一条记录代表一个学生选了一门开课,成绩、退课标记都是这条记录的属性。下面这段 Django 模型是这类系统里最常见的地基,解压出来的项目十有八九是类似的表结构。

# students/models.py —— 教务系统最核心的几张表 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="姓名") major = models.CharField(max_length=50, blank=True, verbose_name="专业") class Teacher(models.Model): name = models.CharField(max_length=50, verbose_name="姓名") title = models.CharField(max_length=20, blank=True, verbose_name="职称") class Course(models.Model): code = models.CharField(max_length=20, unique=True, verbose_name="课程代码") name = models.CharField(max_length=80, verbose_name="课程名") credit = models.DecimalField(max_digits=3, decimal_places=1, verbose_name="学分") class CourseOffering(models.Model): course = models.ForeignKey(Course, on_delete=models.CASCADE, verbose_name="课程") semester = models.CharField(max_length=20, db_index=True, verbose_name="学期") teacher = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True) capacity = models.PositiveIntegerField(default=60, verbose_name="容量") schedule = models.CharField(max_length=200, verbose_name="上课时间,如 Mon-1-2|Wed-3-4") class Enrollment(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE, verbose_name="学生") offering = models.ForeignKey(CourseOffering, on_delete=models.CASCADE, verbose_name="开课") score = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True, verbose_name="成绩") dropped = models.BooleanField(default=False, verbose_name="是否退课") class Meta: unique_together = (("student", "offering"),)

这段代码里值得注意的参数有三个。student_no 的 unique=True 保证学号不重复,这是教务系统的硬约束。CourseOffering 把「课程」和「某学期某老师开的班」分开,否则排课数据没法设计。Enrollment 的 unique_together 确保同一个学生不能在同一条开课记录下重复选课,比在视图层加判断可靠得多。schedule 字段用逗号分隔存星期和节次,小型系统里够用,量大了再拆时间表,别一开始就过度设计。

为什么不把成绩直接放进 Student 或 Course?因为一次选课先没有成绩,考完才有,单独建一张成绩表的话每次查询都得联表,徒增复杂度。成绩作为 Enrollment 的可空字段是最省事的,查成绩单就是一次 filter 加一个 select_related,性能和维护成本都最低。

2.2 Django 还是 Flask:选错了后面每改一个功能都在还债

这个 zip 里到底是 Django 还是 Flask,直接决定你怎么改。我见过的「python学校教务系统」类工程,八到九成是 Django,原因是教务系统本质上是 CRUD 密集型业务,Django 的 admin 后台、自带 ORM 和迁移工具能省掉一半工作量。

维度DjangoFlask
后台管理自带 admin,注册模型就能给教务员用需要自己拼 Flask-Admin
ORM 和迁移内置,python manage.py migrate 一条龙要装 SQLAlchemy + Alembic
用户权限Auth 和 Group/Permission 开箱即用Flask-Login 加手写装饰器
适合什么教务、OA、进销存这类表单密集业务前后端分离 API、定制小服务

判断方法不用装包,直接看 requirements.txt。里面有 django 就是前者;只有 flask 的,就要做好自己补用户系统和后台的心理准备。数据库方面,开发阶段用 SQLite 能省事,交付上线还是换 MySQL 8。Windows 上装 MySQL 8 通常走 zip 免安装包路线:解压官方 zip 包、写 my.ini、mysqld --initialize-insecure、mysqld install、net start mysql,这套流程和本项目解压 zip 是同一种思路,后面第 3 章会带一遍。

提示:如果 zip 解压后连 requirements.txt 都没有,这个包大概率是作者随手导出的,你要先按 manage.py 的 import 反推依赖,再补一份依赖清单,否则换个机器就跑不起来。

3. 把 zip 里的项目跑起来:环境、依赖、数据库到本地启动的完整命令

这一章的目标只有一个:让系统在本地浏览器里跑起来。别上来就 runserver,环境没隔离开,后面装一个新包就可能把整个 Python 弄脏,那是最常见的新手翻车现场。

3.1 Python 环境准备:虚拟环境是后悔药

网上 python 安装教程讲的是装解释器,这里更关键的是装完解释器之后那一步——建虚拟环境。虚拟环境的本质是把项目依赖装到一个独立的 site-packages 目录里,不和系统全局库互相污染。想知道某个包装在哪个目录下,venv 里直接用 pip show 包名就能看到路径。

# 进入解压后的项目目录 cd python学校教务系统 # 创建虚拟环境;Linux 下 python3 -m venv venv,Windows 用 python -m venv venv python -m venv venv # 激活:Windows 是 venv\Scripts\activate,Linux/macOS 是 source venv/bin/activate source venv/bin/activate # 安装依赖,第一次建议走国内镜像源提速 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

激活之后命令行提示符前面会出现 (venv),这就是隔离生效的标志。如果用的是 VSCode,记得把 Python 解释器指到 venv 目录,否则 F5 调试跑的还是全局环境,经常出现「终端里能跑,点调试就报 ModuleNotFoundError」的怪问题。镜像源参数 -i 后面跟的是 PyPI 镜像地址,只对当前这条安装命令生效;想让环境默认走镜像,用 pip config set global.index-url 设置一次即可。

依赖装完先别急着启动,先确认没有漏装。方法很笨但有效:python -c "import django" 逐个试,报 ModuleNotFoundError 就现场补装。这一步骤省不了,因为很多 zip 包的 requirements.txt 根本不全。

3.2 建库和导入初始数据:连不上库就全白搭

教务系统一般配 MySQL,settings.py 里的 DATABASES 字典指明了连哪个库。如果 zip 里带 .sql 或 fixture 数据,说明作者把基础数据准备好了,导入之后立刻有账号可以登录。

# 建库:字符集用 utf8mb4,否则中文姓名存不进去 mysql -uroot -p -e "CREATE DATABASE school_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 如果 zip 里带 dump.sql,直接导入基础数据 mysql -uroot -p school_system < dump.sql # Django 工程:执行迁移,生成项目自带表(auth、session 等) python manage.py migrate # 创建教务员超级账号 python manage.py createsuperuser

settings.py 里 DATABASES 的配置要改三个地方:NAME 改成 school_system,USER 和 PASSWORD 改成你自己 MySQL 的账号密码,HOST 如果是本机就写 127.0.0.1。如果项目用的是 PyMySQL 驱动,还要确认 MySQL 8 的认证插件兼容,这个坑在第 5 章展开。migrate 跑完,项目自带的数据表会全部建好;dump.sql 导入报错的话,多半是 SQL 文件编码和建库时不一致,重新用 utf8mb4 建一次库再导。

3.3 用冒烟脚本验证核心流程:别靠浏览器一点点点

启动开发服务器之后别急着截图交差,写一个几十行的冒烟脚本把登录、查课表、录成绩三件事一次性验证掉,比手工点网页快得多,也更容易向别人证明系统是通的。

python manage.py runserver 127.0.0.1:8000 --noreload
# verify_api.py —— 三个接口各测一次,失败直接给出明确报错 import requests BASE = "http://127.0.0.1:8000" s = requests.Session() # 1. 教务员登录(路径以项目 urls.py 为准) r = s.post(f"{BASE}/api/auth/login/", json={"username": "admin", "password": "admin123"}) assert r.status_code == 200, f"登录失败: {r.status_code} {r.text}" # 2. 查当前学期开课列表 r = s.get(f"{BASE}/api/courses/", params={"semester": "2025-2026-1"}) assert r.status_code == 200 and r.json().get("count", 0) > 0, "课程列表为空" # 3. 给一条选课记录录成绩 r = s.patch(f"{BASE}/api/enrollments/12/", json={"score": "88.5"}) assert r.status_code == 200, f"录成绩失败: {r.status_code} {r.text}" print("教务系统冒烟测试通过")

这段脚本有三个参数要按项目实际情况改:登录接口路径、学期字段的写法、选课记录 ID。接口路径别猜,先看 urls.py 或直接 python manage.py show_urls 拉一份路由表。requests 库没装的话先 pip install requests,它是独立于 Django 的测试依赖,不会污染项目环境。

4. 改业务代码的重头戏:选课冲突、批量成绩录入和角色权限

跑通之后才是真活。教务系统改得最多的是三个点:选课时的时间冲突和容量控制、期末批量录成绩的性能、以及教师/学生/教务员三种身份的权限。这三个点都从 Enrollment 这张核心表出发,改起来有套路可循。

4.1 选课冲突检测的两个硬约束:时间片和容量

选课逻辑最容易写出 Bug 的地方是冲突检测。时间冲突的本质是两个开课的时间片有交集,容量冲突的本质是已选人数达到上限。两者都要在写入数据库之前判断,并且要防并发。

# services/course_service.py —— 选课核心服务 from django.db import transaction from django.core.exceptions import ValidationError def parse_slots(schedule): """把 'Mon-1-2|Wed-3-4' 解析成 {(星期, 节次)} 集合""" slots = set() for item in schedule.split("|"): day, _, periods = item.partition("-") for p in periods.split("&"): slots.add((day, p)) return slots def enroll(student, offering_id): with transaction.atomic(): # 锁行,防止并发选课超容量 offering = CourseOffering.objects.select_for_update().get(pk=offering_id) if offering.enrollment_set.filter(student=student, dropped=False).exists(): raise ValidationError("你已经选过这门课") if offering.enrollment_set.filter(dropped=False).count() >= offering.capacity: raise ValidationError("课程容量已满") new_slots = parse_slots(offering.schedule) for chosen in offering.enrollment_set.filter(student=student, dropped=False): if parse_slots(chosen.offering.schedule) & new_slots: raise ValidationError(f"与 {chosen.offering.course.name} 时间冲突") return Enrollment.objects.create(student=student, offering=offering)

这套代码里三个关键点。select_for_update() 必须在 transaction.atomic() 块内才生效,它把当前行锁住,两个学生同时选最后一门课时,第二个请求会等到第一个提交再判断容量,避免超卖。parse_slots 把时间字符串转成集合,后面用集合交集判断冲突,一次运算就能出结果,不用嵌套循环。容量判断用的是 count(),记录量大了记得给 dropped 字段加索引,否则百万级选课记录会让 count 变成全表扫描。

4.2 批量录入成绩:用 bulk_update 避免 N+1 地狱

期末录成绩的场景是教务系统性能问题的高发区。最常见错误是循环里逐条 save(),几百条成绩就要发几百次 SQL,页面转圈转到教务员想砸电脑。批量更新能一次请求把数据写回数据库。

# services/score_service.py —— 批量导入成绩 from decimal import Decimal def import_scores(offering_id, rows): """rows: [{'student_no': '20230001', 'score': '88.5'}, ...]""" enrollment_map = {} for e in (Enrollment.objects .filter(offering_id=offering_id, dropped=False) .select_related("student")): enrollment_map[e.student.student_no] = e to_update = [] skipped = [] for row in rows: e = enrollment_map.get(row["student_no"]) if not e or row["score"] in (None, ""): skipped.append(row["student_no"]) continue e.score = Decimal(row["score"]) to_update.append(e) Enrollment.objects.bulk_update(to_update, ["score"], batch_size=500) print(f"更新 {len(to_update)} 条,跳过 {len(skipped)} 条")

bulk_update 会把所有变更合并成一条批量 UPDATE 语句,batch_size=500 是每次写入的条数,太大容易撑爆数据库连接内存。select_related("student") 是为了拿到学号时不再多查学生表,两处合起来省掉了一个数量级的数据库往返。学号对不上或成绩为空的记录先收集起来打印,别静默跳过,否则教务员核数据时得骂人。

导成绩单是同一个模块的另一个出口。教务处的老师要的是 xlsx,不是网页表格,用 openpyxl 写 Excel 不会踩 CSV 的编码坑:

from openpyxl import Workbook def export_scores(offering_id, filepath): wb = Workbook() ws = wb.active ws.append(["学号", "姓名", "成绩"]) for e in (Enrollment.objects .filter(offering_id=offering_id, dropped=False) .select_related("student", "offering__course")): ws.append([e.student.student_no, e.student.name, str(e.score or "")]) wb.save(filepath)

4.3 三种角色的权限控制:从 request.user 到装饰器

教务系统的权限分三类:教务员能管理一切,教师能录入自己课程的成绩,学生只能看自己的选课和成绩。zip 里如果用的是 Django 自带 auth,request.user 就是登录用户,剩下的问题是怎么快速限制视图的访问角色。

# utils/permissions.py —— 角色装饰器 from functools import wraps from django.http import JsonResponse def role_required(*roles): def decorator(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): user = request.user if not user.is_authenticated: return JsonResponse({"error": "未登录"}, status=401) if not user.is_superuser and getattr(user, "role", None) not in roles: return JsonResponse({"error": "无权访问"}, status=403) return view_func(request, *args, **kwargs) return wrapper return decorator

用法是把 @role_required("admin") 记在视图函数上,教务员专属的接口就封住了。但这里有个先决条件:Django 默认的 User 模型没有 role 字段。要么在项目里建一个 Profile 表一对一存角色,要么在最初建表时就设置 settings.AUTH_USER_MODEL 指向自定义用户模型。已经跑过 migrate 的现成项目再改自定义用户模型非常痛苦,所以拿到 zip 先看 settings.py 里有没有 AUTH_USER_MODEL,没有的话就用 Profile 方案,别轻举妄动。

注意:前端按钮隐藏不等于权限控制,接口后端一定要校验。我们见过学生直接调接口把自己成绩改成 100 的案例,前端藏得再好也没用。

5. 从解压到上线最容易翻车的 5 个坑:编码、依赖和数据库的血泪记录

这一章把这类项目最常见的五类报错一次讲完,每条都按「现象 → 原因 → 解决」写,照着排查能省一个晚上。

5.1 zip 伪加密和 EOCD 错误:解压报错不一定是包坏了

现象:解压时弹出要密码,或者 7-Zip 直接报 "invalid zip archive: could not find EOCD"。原因:网上流传的源码包常用「伪加密」手法,把压缩包文件头的加密标志位改成 1,数据其实没加密,纯粹为卡住新手;EOCD 是压缩包结尾记录,找不到它多半是文件不完整或被人为改过。解决:用下面这段脚本把加密标志位改回去,另存成新 zip 再解压。

# fix_zip.py —— 修复 zip 伪加密,把文件头加密位清零 import struct def fix_fake_encryption(zip_path, out_path): with open(zip_path, "rb") as f: data = bytearray(f.read()) fixed = 0 i = 0 while i < len(data) - 4: # 中央目录文件头签名固定为 PK\x01\x02 if data[i:i+4] == b"PK\x01\x02": flag = struct.unpack_from("<H", data, i + 8)[0] if flag & 0x0001: # bit 0 是加密标志 struct.pack_into("<H", data, i + 8, flag & ~0x0001) fixed += 1 i += 4 continue i += 1 with open(out_path, "wb") as f: f.write(data) print(f"修复 {fixed} 个文件头,输出 {out_path}")

这段代码遍历中央目录,把每个文件头的通用位标记第 0 位清零。如果修复后仍提示要密码,说明是真加密,改标志位没用,得靠 zip 密码移除工具跑字典,或者直接找作者要密码。CTF 的 misc 方向也常拿伪加密出题,判断标志位的逻辑一模一样。顺带提醒:文件下载下来先看大小,几 KB 的源码包基本是坑,别浪费时间。

5.2 Windows 下中文乱码:CSV 和代码文件的编码战争

现象:导入学生名单 CSV 后页面全是乱码,或者程序直接报 UnicodeDecodeError。原因:Windows 上 Excel 导出的 CSV 默认 GBK 编码,Python 3 读写文件默认 UTF-8,两边对不上就翻车。解决:读写都显式指定编码,写入时用 utf-8-sig 带上 BOM,Excel 双击打开才不会乱。

import csv # 读 CSV:utf-8-sig 能兼容带 BOM 的文件,也能读普通 UTF-8 with open("students.csv", newline="", encoding="utf-8-sig") as f: for row in csv.DictReader(f): print(row["学号"], row["姓名"])

如果确认数据源是 GBK,就把编码参数换成 gbk,别用 errors="ignore" 糊弄,那会把中文名直接吞掉。另外 .py 文件本身在 Python 3 里默认 UTF-8,文件头加 # -- coding: utf-8 -- 已经是历史遗留写法;真正坑人的是记事本另存为时加的 BOM 头,代码文件开头一旦有 BOM,Python 有时会把 BOM 当字符报语法错误。

5.3 requirements.txt 版本冲突:装包一时爽,跑起来火葬场

现象:新环境里 pip install -r requirements.txt 能装完,一启动就报版本不兼容。原因:依赖清单里要么是 >= 太松,装到了不兼容的新版;要么 == 太死,作者用的老版本和当前 Python 编译不兼容。解决:在自己机器上验证通过后,用 pip freeze 生成固定版本清单,只保留项目真正用到的包。

# 在干净的 venv 里验证完,导出精确版本 pip freeze | grep -Ei "django|mysql|openpyxl|celery|requests" > requirements.lock pip install -r requirements.lock

依赖问题最玄学,同样的代码换个机器就炸,所以固定版本是这类项目交付时的底线要求。常见救急手段是顺藤摸瓜:看到 Django 版本后,去对应的 Release Notes 查兼容矩阵。如果项目要用 mysqlclient 但 Windows 上编译失败,改用 pymysql,在项目init.py 里加一句 pymysql.install_as_MySQLdb(),这是最省事的替代方案,前提是 Django 版本别太老。

5.4 MySQL 8 的 caching_sha2_password:服务能起,连接报错

现象:MySQL 服务正常,但 Django 一启动就报 "Authentication plugin 'caching_sha2_password' cannot be loaded"。原因:MySQL 8 默认认证插件改成了 caching_sha2_password,老驱动和旧客户端不认识。解决:把账号认证方式改回 mysql_native_password,或升级驱动到支持新插件的版本。

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

这句 SQL 只改 root 这一个账号,生产环境不要这么干,建一个专用的教务系统账号再改。如果升级驱动之后问题还在,检查连接 URL 里有没有加 charset=utf8mb4,编码不匹配会导致中文查询结果诡异,这种问题看起来像业务 Bug,实际是驱动参数没到位。

5.5 DEBUG=False 之后静态文件 404:一键发布就裸奔

现象:本地 runserver 一切正常,一关 DEBUG 或部署到服务器,CSS、JS 全部 404。原因:Django 开发服务器包办了静态文件服务,DEBUG 关闭后这套机制停止工作,静态文件没有对外提供。解决:用 whitenoise 中间件,或让 Nginx 直接指向静态文件目录。

# settings.py 中,DEBUG=False 时补上这个中间件 MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", # ... 其余中间件 ] STATIC_ROOT = BASE_DIR / "staticfiles"

改完配置执行 python manage.py collectstatic,把散落在各 app 的静态文件收集到 STATIC_ROOT。静态文件 404 是部署问题不是代码问题,排查时先用浏览器 F12 看请求返回码,404 就去查中间件和 collectstatic,500 再查服务日志。

6. 把教务系统从"能跑"改到"敢用":三个验证和交付技巧

系统跑起来只是开始,敢把数据交给它是另一回事。这一章讲三个我每次交付前都会做的验证动作。

6.1 用 Django admin 做快速数据维护

如果 zip 里是 Django 项目,admin 就是给教务员用的免费后台。打开 admin.py 注册模型,配置 list_display 和 list_filter,教师、课程、选课记录就能在网页里直接改,不用给他们开数据库权限。

# students/admin.py from django.contrib import admin from .models import Student, CourseOffering, Enrollment @admin.register(Enrollment) class EnrollmentAdmin(admin.ModelAdmin): list_display = ("student", "offering", "score", "dropped") list_filter = ("offering__semester", "dropped") search_fields = ("student__student_no", "student__name")

6.2 上线前的备份和压测

给系统配一个定时备份,mysqldump 加压缩,每天一次,出问题才有后悔药。

0 2 * * * mysqldump -uroot -p"密码" school_system | gzip > /data/backup/school_$(date +\%F).sql.gz

压测别用高端工具,直接把第 3 章的 verify_api.py 加个循环跑 200 次选课请求,观察响应时间有没有随并发线性恶化。重点压选课接口,它同时读容量和写记录,是教务系统里最有可能出性能问题的点。

6.3 日志和监控:交付后还能定位问题

给 settings.py 配一个文件日志,记录到 logs/app.log,滚动保留 7 天。交付文档里写明三个必看位置:Django 日志、MySQL 慢查询日志、备份文件目录。我的习惯是每次改完业务代码,先跑一遍 verify_api.py,再跑一轮备份脚本,确认两条红线都通才收工。这个不起眼的动作救过我很多次,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询