
简介Python学校教务系统是一份面向Python初学者的完整Web开发实践项目覆盖学生信息管理、课程安排、成绩录入与查询等核心教务场景适合作为课程设计、毕业设计或框架入门练习。压缩包共9个文件包含1个主程序文件、7个数据库初始化脚本和1份配套论文文档总大小仅467KB结构清晰便于快速部署和二次开发。其中数据库脚本分别定义了用户、学生、选课等多个数据表主程序负责业务逻辑与页面交互论文则记录了从需求分析、系统设计到功能测试的完整过程。该资源已有592人学习下载内容完整参考价值较好。通过实际运行和修改可掌握Python基础语法、常用Web框架的使用、SQL增删改查、前后端数据交互以及系统设计流程对希望借助真实项目提升编码能力的学习者尤为实用。1. 学校教务系统用 Python 写到底值不值得自己动手拿到「python学校教务系统.zip」这个标题先别急着解压。很多人在毕业设计、课程作业或者帮学校信息中心做内部工具时第一反应是找一个现成压缩包跑起来改个数据库连接字符串就交差。这个思路没错但坑也很深网上流传的教务系统包十有八九是 Django 2.x Python 3.6 时代的产物有的甚至带着 MySQLdb 这种老古董驱动放到现在的 Python 3.11、3.12 环境里根本装不上依赖。更麻烦的是很多包的权限模型和选课逻辑是写死的塞进真实学校场景会出现各种边界问题比如选课冲突判断不完整、成绩录入没有操作日志。我这篇笔记准备用一套可复现的方案把一个基于 Python 的学校教务系统从解压到改造、从本地调试到部署排错完整走一遍。它的核心价值不是某个现成代码包而是把教务系统里的那些硬骨头——选课冲突检测、成绩权重计算、权限分级、导出报表——用 Python 生态里最成熟的工具链重新搭一遍。适合三类人一是做毕设需要快速出活的学生二是想给学校或培训机构做内部选课小系统的管理者三是想搞明白教务系统背后业务模型的开发者。读完之后你至少能判断手里那个 zip 是能用还是要扔也能自己动手补上关键模块。2. 拆解教务系统的核心模型从 zip 包到可运行系统2.1 教务系统的业务边界不是「增删改查」那么简单很多人拿到教务系统压缩包打开项目结构一看有 students、courses、grades 三个 model 就以为懂了。实际上学校的教务系统最少要覆盖六个核心流程学生信息管理、教师授课安排、课程库维护、选课与退课、成绩录入与审核、统计报表导出。每一个流程都不是简单的数据库表操作。举个例子选课这个动作表面上是在选课记录表里插入一行背后却要处理容量检查、时间冲突检查、先修课程校验、选课时段开关甚至还有学分上限判断。一个做得合格的系统选课接口里至少要有六七层校验逻辑而网上流传的很多精简包往往只做了容量判断时间冲突直接忽略。另一个容易被忽略的是成绩模块。成绩不能由任课教师直接提交就生效通常需要教研室主任审核、教务处归档两步。这就在成绩表上引入了状态字段暂存、已提交、已审核、已归档。很多现成包把成绩做成了简单的增删改查老师说改就改最后教务处导出报表时数据一塌糊涂。我自己接过一个培训机构的内部系统就是因为成绩没有审核流期末核算时才发现有老师把平时分和期末分填反了数据已经覆盖根本追溯不回去。所以在动手改代码之前先把业务模型理顺。我的习惯是用一张表格把角色、动作、权限列清楚这样不管是读别人的代码还是自己重写都有据可依。角色核心动作权限边界学生选课、退课、查成绩只能操作自己的记录不能修改已归档数据教师录入成绩、查看授课名单只能操作自己课程的成绩提交后需审核教务员维护课程库、排课、选课开关可管理基础数据但不能改成绩管理员用户管理、角色分配、系统配置全权限但建议保留操作日志这个表格看起来简单但真去对照现成代码会发现大部分系统把教师和管理员的权限混在一起学生甚至能直接访问 Django admin 后台。这是第一个要处理的问题。2.2 解压后第一步用虚拟环境隔离依赖别直接 pip install打开「python学校教务系统.zip」常见做法是解压后直接pip install -r requirements.txt然后python manage.py migrate。这个流程在 Python 3.6 时代没问题但在当前环境下大概率报错。原因有两个一是老包依赖的 django 版本可能停留在 1.x 或 2.x和新的 Python 不兼容二是 requirements.txt 里通常没有锁版本号pip 会装最新版结果 Django 4.x 的 url 配置语法和原来代码完全不一样启动直接报错。我一般会这么做先创建独立的虚拟环境再手动指定一个相对稳妥的 Django 版本最后逐个安装依赖。假如你手里的包用的是 Django 2.2那就固定装 django2.2.28这个版本能兼容 Python 3.6 到 3.9比较稳定。如果代码里用了老式的url()函数或者django.conf.urls.url说明这个包很可能是 Django 2.x 时代的作品直接装 4.x 必翻车。cd python学校教务系统 python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install django2.2.28 pip install -r requirements.txt这里的关键是固定主版本。比如 django2.2.28 末尾的 28 是补丁号不影响 API 兼容性但必须保证大版本一致。安装完后不要急着 migrate先执行python manage.py check检查配置是否有语法错误。这一步能提前暴露 URL 配置、模型字段等老代码问题比 migrate 之后报错更容易定位。如果你的包是 Django 4.x 写的那就装最新的 django4.2.x语法风格完全不同。判断办法很简单打开项目目录下的 urls.py如果看到re_path或path函数是 2.0 以上如果看到url(是 1.x 或 2.x 早期。2.3 数据库迁移SQLite 起步MySQL 上线前的三个检查绝大多数教务系统包默认配置是 SQLite因为零配置、开箱即用特别适合本地跑通流程。但真实学校环境不可能用 SQLite并发一高就锁库。我常见的做法是本地先用 SQLite 把业务逻辑调通部署前再切到 MySQL。切换不是改一下数据库引擎字符串就行有几个坑必须先处理。第一个坑是字符集。MySQL 建库时如果没指定 utf8mb4存学生姓名或课程名称时碰到生僻字就是问号。我的习惯是建库语句单独执行建好后再让 Django 迁移。第二个坑是 JSON 字段。老版本 Django 在 MySQL 上对 JSONField 支持不完整如果代码里用了需要确认 MySQL 版本在 5.7.8 以上。第三个坑是时间时区。Django 的 USE_TZ 配置如果是 True写入 MySQL 的 datetime 字段会和本地时间差 8 小时这是最常见的玄学问题。# settings.py 中数据库配置示例 DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: school_db, USER: school_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }这段配置里charset 指定 utf8mb4 解决生僻字问题init_command 里的严格模式能保证数据写入时类型不合法直接报错而不是静默截断。很多人忽略 OPTIONS 里的配置结果数据错了都不知道原因。建议在切 MySQL 之前先用本地 Docker 起一个 MySQL 5.7 容器做一遍迁移测试别直接连学校生产库。3. 从 zip 到可用Django 教务系统的代码改造路线3.1 用户模型扩展不要直接改 Django 自带的 User 表绝大多数教务系统包都会用到 Django 自带的 auth.User 做登录然后单独建一个 StudentProfile 或 TeacherProfile 表来存额外信息用 OneToOneField 关联。这个设计是对的但有不完善的地方很多包没有把「学生」和「教师」的区分做成一个可扩展的机制而是在视图函数里用if request.user.student_profile.exists()这种低级判断代码重复率高权限还容易漏。我的改造方案是引入一个 Profile 基类用 role 字段区分身份同时把权限判断做成装饰器。这样每个视图只需要标注允许的角色不用在函数内部写一堆判断。这是一个很小的改动但能让权限模型清晰很多后面加功能时不容易出错。from django.contrib.auth.models import User from django.db import models class BaseProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) role models.CharField(max_length20, choices[ (student, 学生), (teacher, 教师), (admin, 教务员), ], defaultstudent) phone models.CharField(max_length20, blankTrue) class Meta: abstract True class StudentProfile(BaseProfile): student_no models.CharField(max_length20, uniqueTrue) enrolled_year models.IntegerField(default2024) class TeacherProfile(BaseProfile): teacher_no models.CharField(max_length20, uniqueTrue) title models.CharField(max_length50, blankTrue)这里把 StudentProfile 和 TeacherProfile 都继承 BaseProfile公共字段放在基类里用 abstractTrue 表示不单独建表。实现的逻辑是一个 User 对应一个 profile而 profile 的 role 决定了系统里能做什么。视图层配合装饰器比在模板里判断user.is_superuser好维护得多。改完模型后一定要执行python manage.py makemigrations accounts python manage.py migrate因为 User 表结构没变但 profile 表新增了字段必须同步。3.2 选课冲突检测时间表用 JSONField 存储校验逻辑单独写函数选课是教务系统里最容易出逻辑漏洞的地方。很多参考系统把上课时间存成一个字符串比如周一 3-4 节然后程序只能做精确匹配判断不了「周一 3-4 节」和「周一 4-5 节」是否冲突。要让系统可靠时间信息必须结构化。我的方案是给课程表加一个 JSONField 字段存储每周的上课时间段列表格式是[{weekday: 1, start: 3, end: 4}, {weekday: 3, start: 1, end: 2}]weekday 1 到 5 表示周一到周五start 和 end 表示节次。冲突检测函数只需要遍历学生已选课程的时间段判断是否有重叠。def check_time_conflict(course, student): new_slots course.schedule # 假设是 JSON 格式 selected Enrollment.objects.filter(studentstudent, statusactive) for enrollment in selected: old_slots enrollment.course.schedule for new_slot in new_slots: for old_slot in old_slots: if new_slot[weekday] old_slot[weekday]: if not (new_slot[end] old_slot[start] or new_slot[start] old_slot[end]): return False, f与课程 {enrollment.course.name} 时间冲突 return True, ok这段逻辑虽然嵌套三层循环但实际数据量很小学生同时选的课不会超过十门性能完全不是瓶颈。注意判断重叠的条件用的是「两个区间不相交」的反面新课程结束节次早于旧课程开始节次或者新课程开始节次晚于旧课程结束节次才说明不冲突反之就有重叠。这个边界条件极其容易写反写反的后果是冲突的课能选上不冲突的反而被拦截。3.3 成绩录入与审核流状态机是教务系统的护城河成绩表只有分数一个字段是完全不够的。真实场景中教师录完分数要核对核对后提交教务员审核最后归档。每一步都要留痕。我常用状态字段模拟一个轻量级状态机class Grade(models.Model): enrollment models.OneToOneField(Enrollment, on_deletemodels.CASCADE) score models.DecimalField(max_digits5, decimal_places2, nullTrue, blankTrue) status models.CharField(max_length20, defaultdraft, choices[ (draft, 暂存), (submitted, 已提交), (approved, 已审核), (archived, 已归档), ]) submit_time models.DateTimeField(nullTrue, blankTrue) approve_time models.DateTimeField(nullTrue, blankTrue)这里状态流转是单向的draft 可以改分数submitted 之后不能再随意修改如果发现录错需要走「退回」动作让状态回到 draft。这个逻辑是业务核心比任何花哨功能都重要。我在做某培训机构系统时就是因为一开始没做状态机期末核算时数据对不上最后只能恢复数据库备份血泪教训。视图层的控制很简单只有 status 为 draft 时教师才能改分数提交后只有教务员能执行 approve 操作。判断逻辑放在视图函数里不能在模板里判断因为模板控制不了 POST 请求。4. 把系统跑起来最小可用配置与启动排错4.1 本地启动用 Django 开发服务器验证功能的最小命令改造完模型和核心逻辑后先别急着加功能把系统跑起来做端到端验证。我的习惯是分三步先建超级用户再创建测试课程最后模拟学生选课。Django 自带的开发服务器足够支撑这个验证流程。python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 127.0.0.1:8000migrate 之前先 makemigrations因为前面改过模型系统会把 model 变化生成新的迁移文件。createsuperuser 创建的账号是 admin 权限可以登录后台管理。runserver 启动后浏览器访问 127.0.0.1:8000 就能看到系统页面。注意创建超级用户后还要用 Django admin 或 shell 命令创建一个 StudentProfile 关联到某个普通用户否则测试学生登录时会出现 profile 不存在的情况。如果启动时报错最常见的两种一是端口被占用改 8001二是依赖没有装全看 traceback 里 ImportError 的模块名回去补装。这一步是最考验耐心的因为老代码的报错信息往往不直观。4.2 依赖冲突排查用 pip list 快速定位版本问题教务系统 zip 包里的 requirements.txt 有时候本身就有问题。比如里写的是Django1.11没有上限pip 到了 2024 年就会装最新的 4.x代码里全是老语法启动报错一大片。我的做法是不直接信任这个文件先人工判断项目用的 Django 大版本然后手动装一次。pip list pip show djangopip show django 会显示当前装的版本如果是 4.2 而代码里用的是老式 url 写法那就降级。降级命令pip install django2.2.28会覆盖当前版本。另外注意如果项目依赖 mysqlclient这个包在 Windows 上装经常失败建议换用 PyMySQL然后把 settings.py 里的数据库驱动改成 pymysql 兼容模式。判断方法很简单安装时报Failed building wheel for mysqlclient就换 PyMySQL 方案。4.3 初始数据准备用 fixture 快速填充课程和学期数据测试阶段手动一条条录入课程太浪费时间我建议用 Django fixture 准备一份模拟数据。fixture 本质是 JSON 或 YAML 格式的数据文件通过 loaddata 命令导入。这样每做一次环境重建都能快速恢复数据。python manage.py dumpdata courses.Course courses_fixture.json python manage.py loaddata courses_fixture.jsondumpdata 会从当前数据库导出指定 app 和 model 的数据loaddata 是逆向导入。如果你没有现成数据可导那就手动用 Python shell 写一段脚本批量生成模拟课程from courses.models import Course import json, random for i in range(20): Course.objects.create( namef示例课程{i}, codefC{i:03d}, creditrandom.choice([2, 3, 4]), capacityrandom.randint(30, 100), schedulejson.dumps([{weekday: random.randint(1, 5), start: random.randint(1, 8), end: random.randint(1, 8)}]) ) print(模拟课程创建完成)这段代码放在任意 Django shell 环境里执行例如python manage.py shell下粘贴运行。注意 schedule 字段如果是 JSONField可以直接传入 list 而不需要手动 json.dumps具体看模型定义。很多包里的 schedule 是 TextField那就必须用 json.dumps 序列化成字符串。我一般建议在模型里就用 JSONFieldDjango 会自己处理序列化省事不少。5. 数据完整性与并发踩坑选课高峰期最容易翻车的地方5.1 超卖问题选课名额 100为什么能选进 101 个人选课系统的经典缺陷是超卖。学生 A 和 B 同时提交选课请求程序先查容量是否满如果未满则插入选课记录。两个请求同时通过容量检查就会插入两条记录课程超员。这个问题的根源是「检查后插入」不是一个原子操作在高并发下必然出现。解决办法有两个层面。轻量方案是用 Django 的 select_for_update 锁住课程记录让同一时刻只有一个事务能修改容量。重量方案是引入 Redis 做原子扣减但学校内部系统流量不大select_for_update 足够。from django.db import transaction def enroll_course(course_id, student): with transaction.atomic(): course Course.objects.select_for_update().get(pkcourse_id) enrolled_count Enrollment.objects.filter(coursecourse, statusactive).count() if enrolled_count course.capacity: raise ValueError(课程名额已满) Enrollment.objects.create(coursecourse, studentstudent, statusactive)select_for_update 的原理是给这一行记录加行级锁第二个事务必须等第一个事务提交或回滚后才能读取所以不会出现两个事务同时通过容量检查。注意这个方法必须在 transaction.atomic() 块内使用单独调用会报错。实际部署时还要配合 MySQL 的 InnoDB 引擎MyISAM 不支持行锁这是另一个坑。5.2 成绩并发编辑两个教师同时录同一门课怎么办成绩录入的并发问题相对少见但不是没有。比如一门课有多个教学班如果代码里没有区分授课教师和课程 ID 的关系可能出现两个教师同时编辑同一个 Grade 记录后提交的覆盖前提交的。更常见的场景是教师自己开两个浏览器标签页同一条记录被改两次。解决思路是在 Grade 模型里加一个 updated_at 字段提交时比较前端记录的时间和数据库里的最新时间如果不一致就让用户刷新页面再操作。这个方案叫乐观锁学校内部系统完全够用不需要引入复杂的事务隔离级别。from django.utils import timezone def save_grade(request, grade_id): grade Grade.objects.get(pkgrade_id) client_updated request.POST.get(updated_at) if grade.updated_at.strftime(%Y-%m-%d %H:%M:%S) ! client_updated: return JsonResponse({error: 数据已被他人更新请刷新后重试}, status409) grade.score request.POST.get(score) grade.updated_at timezone.now() grade.save()这里比较的是字符串形式的时间因为浮点时间在网络传输中可能丢失精度。核心思想是谁先提交谁生效后提交的人得到 409 冲突提示而不是静默覆盖。这种做法在排课系统里同样适用比如教务员同时调整两个班的上课时间也应该用类似机制。5.3 事务回滚与日志出问题时后悔药在哪里教务系统最怕的就是数据改错了、找不回来。没有日志的系统就是没有后悔药。我的经验是从第一天开发就加上操作日志和 django-import-export 这类备份导出工具。操作日志记录谁在什么时候改了什么数据upredictable 的现场问题可以通过日志还原过程备份导出则可以定期把核心表导出成 Excel 或 JSON 文件存到系统外面。class OperationLog(models.Model): user models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) action models.CharField(max_length200) object_repr models.CharField(max_length200) detail models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue)然后把操作日志的写入放到视图层做一个装饰器统一记录。这样每次学生选课、退课、教师改成绩系统都有一条日志。日志表本身也会膨胀定期清理三个月前的记录即可或者只保留错误级别的日志。6. 导出成绩单与统计报表把数据变成教务处能用的东西6.1 用 openpyxl 生成可选课的 Excel 报表教务系统光有页面不够教务处经常要导出 Excel 报表交给上级或存档。最可靠的工具是 openpyxl它能生成带格式的 xlsx 文件支持合并单元格、列宽调整、单元格样式。不要用 CSV因为 CSV 对中文编码支持差打开 Excel 会乱码尤其 Windows 环境。from openpyxl import Workbook from openpyxl.styles import Font, Alignment def export_grades(course_id): wb Workbook() ws wb.active ws.title 成绩单 headers [学号, 姓名, 平时成绩, 期末成绩, 总评, 状态] ws.append(headers) grades Grade.objects.filter(enrollment__course_idcourse_id).select_related(enrollment__student) for grade in grades: student grade.enrollment.student ws.append([ student.student_no, student.user.username, grade.regular_score, grade.final_score, grade.total_score, grade.get_status_display() ]) ws.column_dimensions[A].width 15 ws.column_dimensions[B].width 12 wb.save(fgrades_{course_id}.xlsx)这段代码里有几个细节值得注意。select_related 用一次 SQL 把关联的学生信息取出来避免逐条查询数据库。get_status_display 是 Django model 自带的获取 choice 字段中文显示值的方法比手动判断 status 优雅。列宽设置是给教务员看的体验优化虽然不影响数据正确性但导出文件拿出去不能太难看。6.2 成绩统计的 Python 实现平均分、及格率、分数段分布教务处的报表里通常需要按课程统计平均分、最高分、最低分、及格率、分数段分布。用纯 SQL 硬写会很痛苦用 Python 的 pandas 虽然好用但为了一个统计功能引入 pandas 有点划不来。更轻量的方案是在遍历成绩记录时完成统计。from collections import defaultdict def course_stats(course_id): grades Grade.objects.filter(enrollment__course_idcourse_id, statusapproved) if not grades: return None score_list [float(g.score) for g in grades if g.score is not None] avg sum(score_list) / max(len(score_list), 1) passed len([s for s in score_list if s 60]) score_ranges defaultdict(int) for s in score_list: bucket 90-100 if s 90 else 80-89 if s 80 else 70-79 if s 70 else 60-69 if s 60 else 不及格 score_ranges[bucket] 1 return { count: len(score_list), avg: round(avg, 2), pass_rate: round(passed / max(len(score_list), 1), 2), ranges: dict(score_ranges) }注意及格率里的 60 分线是硬编码实际系统中课程的类型不同及格线可能不同比如某些实操课 60 分及格英语四六级培训类课程可能 425 及格。这个参数应该放进课程表配置而不是写死。6.3 定时备份与本地归档三重备份策略教务系统的数据重要性怎么说都不为过。我在做某培训机构模拟项目X的时候遇到过数据库文件损坏、只能靠备份恢复的情况。我的建议是本地开发、测试、生产环境各自做不同频率的备份本地用 dumpdata 导出 JSON测试环境每天备份 MySQL生产环境至少每天全量备份加 binlog。python manage.py dumpdata --exclude auth.Permission --exclude contenttypes backup_$(date %Y%m%d).json7. 常见问题与避坑排查运行与部署中必踩的 4 个坑7.1 静态文件全 404页面样式全丢现象登录页能打开但没有任何 CSS 样式页面光秃秃的。原因DEBUGFalse 时 Django 不再自动提供静态文件服务而部署时又没有配置 Nginx 代理静态目录。解决在 settings.py 中设置 STATIC_ROOT然后执行 collectstatic再让 Web 服务器指向该目录。STATIC_URL /static/ STATIC_ROOT os.path.join(BASE_DIR, staticfiles)这是部署到生产环境必经的一步。如果只是本地开发把 DEBUG 保持 True 就行不影响调试。7.2 选课记录插入重复超卖问题在 MySQL 和 SQLite 的表现差异现象SQLite 本地测试一切正常部署到 MySQL 后选课偶尔出现超员。原因SQLite 对写锁的处理是整体锁数据库串行化严重反而掩盖了并发问题MySQL 的 InnoDB 在默认隔离级别下允许更高并发问题就暴露出来。解决用第 5 章的 select_for_update 方案同时在课程表加一个实际选课人数的冗余字段并在事务内更新该字段而不是每次 count 遍历。7.3 时区差 8 小时归档成绩单时间对不上现象报表里显示的成绩提交时间比实际时间早 8 小时。原因Django 的 USE_TZ 为 True数据库存的是 UTC 时间前端展示时没有做本地时区转换。检查思路如果只在 Django 模板渲染时间Django 会自动转换如果用 openpyxl 导出 Excel则需要手动转回本地时区。解决在导出 Excel 时用timezone.localtime(graded_at)把时间转换成本地时间再写入单元格。7.4 中文用户名报错MySQL 建表字符集导致数据插入失败现象新建学生账号时姓名含中文保存时报Incorrect string value。原因MySQL 数据库或表的字符集不是 utf8mb4而是 latin1 或 utf8旧版 utf8 不支持生僻字。解决改库表和字段的字符集注意连接字符串也要保持一致。ALTER DATABASE school_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE auth_user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这条 SQL 要针对所有涉及中文的表执行最好的办法是在建库时就指定字符集。如果已经建好执行上述转换命令也能救回来。这里提醒一句utf8mb4 和 utf8 不是同一个东西utf8 在 MySQL 里最多三字节存不了 emoji 和部分生僻字一定用 utf8mb4。8. 进一步可扩展接入 Redis 缓存和基于角色的细粒度权限控制系统跑通之后真正要接上线性能和安全是两个躲不开的点。先说性能选课高峰期所有学生同时刷新课程列表大量的数据库查询会让 MySQL 负载飙升。这时可以引入 Redis 缓存课程列表接口设置 30 秒超时超时后再回源查数据库。选课成功或课程容量变化时主动失效缓存保证数据基本实时。import redis from django.core.cache import cache def get_course_list(request): courses cache.get(course_list) if not courses: courses list(Course.objects.filter(is_activeTrue)) cache.set(course_list, courses, timeout30) return render(request, course_list.html, {courses: courses})这段代码里把查询结果缓存到内存时长 30 秒。前端看到的课程列表最多延迟 30 秒更新对安全性和并发压力的缓解是立竿见影的。注意如果部署环境没有 Redis可以先改用 Django 内置的 LocMemCache效果略差但不引入额外服务。权限方面前面我们做了 base profile 和 role 字段但视图里还是要写request.user.profile.role teacher这类代码。更进一步的做法是用 Django 自带的 Permission 框架或者第三方库 django-guardian 做对象级权限。具体到教务系统对象级权限体现在「教师只能改自己教的课程成绩」这个需求上用装饰器加课程归属判断就能完成。from django.core.exceptions import PermissionDenied def teacher_owns_course(view_func): def wrapper(request, *args, **kwargs): course_id kwargs.get(course_id) course Course.objects.get(pkcourse_id) if course.teacher.user ! request.user: raise PermissionDenied return view_func(request, *args, **kwargs) return wrapper这是最轻量化的对象权限控制也是我平时最常用的做法。权限逻辑集中在装饰器里而不是散落在各个视图函数内部后续加接口时直接标注即可。如果系统规模再大就考虑引入完整的 RBAC 框架但学校内部系统通常没这个必要过度设计会拖累开发速度。我的经验是先把业务跑顺再谈框架化。开发教务系统的这一年多里我最大的教训就是不要一拿到 zip 包就想着赶紧跑起来先用半小时读一遍模型文件和 settings把数据流摸清楚再动手。磨刀不误砍柴工这句话在教务系统这种业务复杂、数据关联紧密的项目上尤其适用。希望这篇笔记里的方案和踩坑记录能帮到你祝你手上的教务系统项目顺利落地。本文还有配套的精品资源点击获取