前阵子接了一个某高校的学生信息管理系统项目甲方技术负责人拉了个清单PHP、ASP.NET、Java、SpringBoot、SSM、Vue3意思是希望我们从中挑一套。我看了半天这个清单说实话有点哭笑不得——这些技术栈各有各的生态但用在学生信息管理这个场景里真正的痛点并不是并发能力也不是社区体量而是“业务关系能不能建模得足够清晰”“权限体系能不能少踩坑”“开发周期能不能在被催死之前做完”。我和团队讨论了几天最终在协议里写下的却是Django框架。这篇文章就把从选型到落地的完整过程记录下来包括数据模型怎么设计、Admin后台怎么用、DRF怎么对接Vue3、以及部署时踩过的坑给正在做类似系统的朋友做一个参考。为什么绕了一圈选了Django不是因为它比SpringBoot高明而是因为它把教务系统最麻烦的几件事提前替我们解决了一部分。下面先从这个项目最真实的起点说起。1. 技术选型背后的真实权衡为什么折腾一圈还是回到Django1.1 学生信息管理系统到底要解决哪些问题先别急着谈技术先把需求搞清楚。学生信息管理系统不是简单的单表增删改查它的核心是“人对人、人对课、课对成绩”的关系网络。站在教务处的角度至少要管好几类数据学生档案、班级、院系、教师信息、课程目录、选课记录、成绩单。学生要查课表、查成绩教师要录成绩、看自己的课教务管理员则要维护所有基础数据还要处理导入导出、统计分析。每个角色能看到的数据范围、能执行的操作完全不同。这种场景有几个非常鲜明的特点。第一数据关系密集一个学生要选多门课一门课有很多学生成绩又同时依赖学生和课程两个维度天然是多对多关系。第二权限层级多且边界清晰至少分学生、教师、教务管理员三种角色而且教务管理员内部往往还要细分出“管学籍的和管成绩的”。第三项目预算和开发周期通常不宽裕很多高校内部系统只有一两个月交付时间中间还要预留出和教务处反复确认需求的时间。这意味着框架要能快速把模型定义出来把后台管理界面搭出来把权限体系跑顺同时还能给外部前端留一个干净的接口。如果只是做一个演示用Demo什么语言都可以但要做到能真正让教务老师日常输入数据就不能只看语言流行度而要看框架对这类业务型应用的覆盖程度。1.2 被考虑过但最终放弃的方案先说PHP。部署确实方便任何一台能跑Apache的机器都能跑。但现代PHP开发如果不引入Laravel这种重量级框架项目后期很容易因为变量写法太自由而变得没法维护引入Laravel之后虽然有Eloquent ORM但内置的后台管理方案几乎都要靠第三方扩展包模型权限的默认支持也不如Django顺手。教务系统里动辄几十张表如果后台管理页面全部手写工期根本撑不住。然后是ASP.NET。它的开发效率其实不低工具链也很完整但问题是团队里没人熟悉Windows服务器运维甲方自己的IT环境又是以Linux为主。为一个校内管理系统去引入整套.NET基础设施后面每年的维护成本都会比框架本身贵得多。除非整个团队已经深度绑定在微软生态里否则在这种Linux环境下选择它并不划算。Java技术栈也就是SpringBoot和SSM是当时差点拍板的方案。SpringBoot的生态非常成熟SSM经典组合也有大量参考文档。但项目启动前需要做的配置实在不少连接池、事务管理器、Mapper扫描、数据源、拦截器注册、统一异常处理……这些东西都有模板可以抄但抄回来通常不理解一旦出了问题排查成本很高。我当时评估了一下一个功能点从写实体类、写Mapper、写Service、写Controller到最终可以调试至少要比Django多出两到三成的工作量。对于两个月就必须交差的校内项目来说这个差别很致命。Vue3不属于后端框架而是前端框架它本来就应该出现在这个项目里。关键是前端联调时后端用什么。如果后端选Java那么接口定义、参数校验、数据返回结构这些都要做一个详细约定光对齐前后端的数据格式就要耗掉好几天如果后端选DjangoDRF的序列化器天然生成规范JSON定义一套序列化器就相当于同时把API文档也基本定了。这里不是Java做不到而是需要投入的沟通成本实在高。这些方案都不是“不能用”只是在这个项目里都不够顺手。我后来跟团队说学生管理系统属于典型的“开发速度优先”的业务系统不是“性能优先”的互联网高并发产品选型标准应该完全不一样。1.3 Django的核心优势如何匹配教务需求Django最吸引我的地方是它几乎把涉密审核系统需要的基础件都内置好了ORM映射、Admin后台、用户认证与权限框架、中间件机制、CSRF保护。这意味着我不需要为了一个登录功能去实现一套Session也不用为了后台管理页面去写一堆CRUD视图。具体到学生信息管理这个场景ORM能直接把Student、Course、Enrollment这些模型定义成类执行迁移就自动建表Admin后台不需要额外写界面注册之后就能录入数据权限框架自带Group和Permission足够支撑角色划分再加上Django REST framework作为API层前端用Vue3对接几乎是天然匹配的。我实测下来团队第一天搭好环境第二天数据模型和Admin就能演示两周后的原型已经包含学生、教师、课程、成绩四个模块的完整增删改查。这个速度用SpringBoot真的很难达到。还有一个隐藏优势是Django的迁移系统。教务系统的表结构经常因为需求变更要加字段Django的makemigrations和migrate能自动生成迁移脚本甚至在已经发生数据变化的表上也能做安全的变更。后来我们确实遇到教务提出“要在学生档案里加一个学籍状态字段”我改完模型执行一次迁移几分钟后线上数据库就更新了。这种迭代效率让整个项目在交付前还能稳定吸收新需求。2. 数据模型是系统的骨架学生、课程、成绩、权限怎么建模2.1 学生信息相关模型设计我习惯先画业务关系再写代码。学生信息管理系统的核心模型大致如下部门Department、班级StudentClass、学生Student、教师Teacher、课程Course、选课记录Enrollment、成绩Score。这里要特别提一句Django模型名字尽量不要直接用Class因为Class是Python关键字虽然Django允许但写代码时很容易出各种奇怪的优先级问题最好改成StudentClass。关键模型代码我贴出来from django.db import models from django.contrib.auth.models import AbstractUser class Department(models.Model): name models.CharField(max_length100, uniqueTrue) code models.CharField(max_length20, uniqueTrue) def __str__(self): return self.name class StudentClass(models.Model): name models.CharField(max_length100) department models.ForeignKey(Department, on_deletemodels.PROTECT, related_nameclasses) grade models.CharField(max_length10) class Meta: unique_together (name, department) class Student(models.Model): student_no models.CharField(max_length20, uniqueTrue) name models.CharField(max_length50) gender models.CharField(max_length2) birth_date models.DateField(nullTrue, blankTrue) student_class models.ForeignKey(StudentClass, on_deletemodels.PROTECT, related_namestudents) enrollment_date models.DateField(nullTrue, blankTrue) def __str__(self): return f{self.student_no} {self.name} class Teacher(models.Model): teacher_no models.CharField(max_length20, uniqueTrue) name models.CharField(max_length50) department models.ForeignKey(Department, on_deletemodels.PROTECT, related_nameteachers) title models.CharField(max_length20, blankTrue) def __str__(self): return f{self.teacher_no} {self.name} class Course(models.Model): code models.CharField(max_length20, uniqueTrue) name models.CharField(max_length100) credit models.FloatField() teacher models.ForeignKey(Teacher, nullTrue, blankTrue, on_deletemodels.SET_NULL, related_namecourses) def __str__(self): return f{self.code} {self.name} class Enrollment(models.Model): student models.ForeignKey(Student, on_deletemodels.PROTECT, related_nameenrollments) course models.ForeignKey(Course, on_deletemodels.PROTECT, related_nameenrollments) semester models.CharField(max_length20) enrolled_at models.DateTimeField(auto_now_addTrue) class Meta: constraints [ models.UniqueConstraint( fields[student, course, semester], nameunique_enrollment_per_semester ) ] class Score(models.Model): enrollment models.OneToOneField(Enrollment, on_deletemodels.CASCADE, related_namescore) score models.FloatField(nullTrue, blankTrue) comment models.CharField(max_length200, blankTrue) updated_at models.DateTimeField(auto_nowTrue)这里有一个非常实用的经验外键的删除策略不要随便用CASCADE。学生档案是历史数据如果误删一个班级导致整批学生被级联删除那基本等于事故。我在这边所有核心外键上都用了PROTECT意思是如果这个记录仍然被其他人引用就直接拒绝删除在Admin后台会返回一个明确的错误信息提醒操作者“还有关联数据”。虽然这样会让操作体验复杂一点但值得。2.2 权限模型扩展Django内置用户体系Django自带的User模型可以直接用于教务系统但它默认只有用户名和密码而且不区分教师和学生。我选择继承AbstractUser添加一个用户身份类型字段并关联到Student或Teacher表。这样做的好处是登录之后可以快速通过user.user_type判断当前来源也能在同一个用户表中统一管理所有账号。class User(AbstractUser): USER_TYPE_CHOICES ( (student, 学生), (teacher, 教师), (admin, 教务管理员), ) user_type models.CharField(max_length10, choicesUSER_TYPE_CHOICES, defaultstudent) student models.OneToOneField(Student, nullTrue, blankTrue, on_deletemodels.PROTECT) teacher models.OneToOneField(Teacher, nullTrue, blankTrue, on_deletemodels.PROTECT)权限上我没有全用Django的细粒度权限而是用Group分组定义一个“学生组”、一个“教师组”、一个“教务管理员组”然后给各组分配Django权限。比如学生组的权限只有查看成绩、查看课程教师组能增加和修改成绩教务管理员组拥有所有基础数据的管理权限。需要特别注意的是Django的权限模型是全局的不支持“某个教师只能查看自己课程的选课记录”这种行级权限。行级过滤必须在View里面手动处理。我们的做法是在Django Admin里自定义get_queryset让教师组的用户只能看到自己课程的选课记录学生组的用户只能看到自己的成绩。在DRF的ViewSet里同理也要重写get_queryset方法。这个逻辑说难不难但容易漏。2.3 为什么用Django的ORM而不是直接SQL一个学生信息管理系统最怕的不是性能慢而是SQL注入和表关系越改越乱。Django的ORM把数据表封装成对象处理学生、选课、成绩这类关系型数据模型时非常顺滑。比如查询某个学生的所有考试科目和分数scores Score.objects.filter(enrollment__studentstudent).select_related(enrollment__course) for s in scores: print(s.enrollment.course.name, s.score)没有SQL拼接也没有代码注入风险。如果需要按班级统计平均分一个annotate加Aggregate就能完成复杂统计from django.db.models import Avg class_average Student.objects.filter(student_classcls).annotate( avg_scoreAvg(enrollments__score__score) )不过我也要坦白有些复杂的报表查询确实用ORM写起来很绕。比如跨学期对比成绩增长率或者涉及多个子查询的排名统计我用connection.cursor()直接执行原生SQL但前提是保证只读查询不破坏模型结构。实际经验是90%的业务用ORM剩下10%用原生SQL这个比例最合理。3. 核心功能模块实现从Admin到DRF再到Vue3的完整链路3.1 先把内部管理后台用Django Admin跑起来Django Admin是这个项目最大的“隐藏福利”。当我把模型注册进admin后台立刻获得了一个可运行的数据管理界面。对于教务管理员来说这比很多定制系统还方便。但直接裸用肯定不行要做一些关键定制。我的配置大概是这样admin.register(Student) class StudentAdmin(admin.ModelAdmin): list_display (student_no, name, student_class, gender, get_department) search_fields (student_no, name) list_filter (student_class__department, student_class__grade) actions [export_excel] def get_department(self, obj): return obj.student_class.department.name get_department.short_description 院系list_display决定后台列表页显示哪些字段search_fields支持按学号和姓名模糊搜索list_filter可以按院系和年级进行筛选。export_excel是我们自己写的一个批量导出action通过openpyxl把当前筛选条件下的学生列表导出成一个Excel文件方便教务处直接导入到自己的统计表里。这个后台的意义在于前端Vue3还没开发完之前校内老师已经开始用后台录入测试数据。到了联调阶段真实数据已经沉淀下来前端页面切过来就能看到预期效果。后期系统正式上线这个Admin后台依然保留给教务内部专用因为它的字段展示、筛选、编辑效率非常高。3.2 RESTful API设计与Vue3前端对接前端选Vue3是没争议的。问题是怎么给前端提供数据。我用的是djangorestframeworkDRF配合ViewSet和ModelSerializer很快把核心接口搭出来。from rest_framework import serializers, viewsets from .models import Student, Course, Score class StudentSerializer(serializers.ModelSerializer): class Meta: model Student fields __all__ class StudentViewSet(viewsets.ModelViewSet): queryset Student.objects.all() serializer_class StudentSerializer permission_classes [IsAuthenticated]路由用DefaultRouter自动注册前端即可通过/api/students/拿到标准JSON数据。登录认证用的是djangorestframework-simplejwt前端发一个POST /api/token/拿到access和refresh token存到localStorage里之后每次axios请求都在header带上Authorization: Bearer token。Vue3这边我用了axios和pinia请求拦截器里统一注入token。和Django后端联调时最一开始就遇到了跨域问题。前端开发服务器跑在http://localhost:5173后端跑在http://localhost:8000浏览器直接拦截。解决方案是加django-cors-headersINSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, django.middleware.common.CommonMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]注意一定要把CorsMiddleware放在CommonMiddleware之前否则CORS头可能不会生效。我在这个坑里卡了一个下午。3.3 选课与成绩管理的业务细节选课模块牵扯到唯一约束。一个学生同一学期同一门课程只能选一次光在应用层判断还不够我还在模型层用了UniqueConstraint这样哪怕并发提交或两条请求同时进来数据库也会拦截重复选课不会产生脏数据。这个设计在Django模型里实现非常简单但效果却非常重要。成绩管理需要注意“分数修改留痕”。教务系统里学生成绩不能老师随手改完就没记录否则出了问题说不清楚。我的做法是在成绩表中增加last_modified_by和last_modified_at字段每次更新成绩时由视图层写入当前用户。如果需要更严格的审计可以再建一张ScoreHistory表通过信号或者重写save方法把旧值和新值都记录下来。还有一个小细节学生查成绩时经常需要看到“这门课的平均分”和“自己在班级里的排名”。这些数据如果让前端临时算既慢又容易出错。我在DRF序列化器里通过SerializerMethodField返回这些聚合数据让前端一次请求就能拿到展示所需的全部信息。这样后端多承担一点计算但对用户和前端团队都很友好。4. 部署、性能优化和踩坑记录4.1 开发与生产配置拆分项目一开始我就在设置目录下拆成base.py、dev.py、prod.py三个文件。dev.py保留DEBUGTrueprod.py设置DEBUGFalse并用环境变量读取数据库密码这类敏感信息避免直接把密码硬编码在代码仓库里。生产部署用的是Linux Nginx uWSGI MySQL。启动命令和配置如下[uwsgi] chdir /srv/student_manage module student_manage.wsgi:application master true processes 4 threads 2 socket 127.0.0.1:8000 vacuum trueNginx端的关键配置是把/api/和/admin/反向代理到uWSGI同时处理/static/和/media/两个路径的静态文件。这里有一个很容易忽视的坑媒体文件学生头像、导入模板等的路径Nginx必须配置location /media/指向Django的MEDIA_ROOT目录否则页面里的图片会全部404。数据库方面一开始用的是SQLite做开发后来迁移到MySQL就出现了一些字段长度和时区的差异问题。所以我建议项目从一开始就直接创建MySQL数据库本地开发也连MySQL省得最后迁移时出一堆编码和类型错误。我在这上面吃过亏所以后面都改成了一开始就统一数据库。4.2 真实线上遇到的几个问题及修法第一个坑是时区问题。Django的TIME_ZONE和USE_TZ如果不协调写入MySQL的时间会少8小时。我们系统只需要使用本地时间所以设置USE_TZFalse并在写入前统一使用datetime.now()。如果以后要做多校区并且跨时区再重新研究和调整。第二个坑是批量导入。教务处一次性给了一张三千人的Excel表我用bulk_create()批量插入速度很快但第一次执行时由于表里有重复学号直接整批失败。后来我改成先读取数据库里已有的学号集合过滤掉重复记录再批量插入existing_nos set(Student.objects.values_list(student_no, flatTrue)) new_records [Student(...) for row in rows if row[student_no] not in existing_nos] Student.objects.bulk_create(new_records, batch_size500)还应该为Excel里每条记录打印出错误行号方便教务修改。第三个坑是JWT过期后前端一直401。默认的ACCESS_TOKEN_LIFETIME只有5分钟用户刷新页面就会觉得系统“掉线”。我把它改成8小时外加refresh机制前端定时刷新一次体验就好了。第四个坑是Django Admin在列表页显示外键字段时性能下降。比如list_display里写了get_department它会在每条记录上额外查询一次班级所属的院系三千条学生记录就会多出三千次查询。解决办法是在StudentAdmin里加上list_select_related (student_class__department,)一次性join出来查询次数瞬间降到个位数。4.3 查询性能优化实操业务数据量到了几万条以后列表接口开始变慢。我记得第一次测试成绩列表接口有学生说“转个圈才出来”当时查了一下响应要800ms左右。我先用django-debug-toolbar看SQL查询数量发现一个列表页竟然发了几十个查询。优化做了三件事。第一给外键和常用筛选字段加db_indexTrue比如Enrollment.semester字段会频繁用于查询加索引后速度提升非常明显。第二所有列表接口开启DRF分页默认每页20条避免一次性返回全量数据。第三详细理解并使用了select_related和prefetch_related。select_related解决多对一关联的查询prefetch_related解决多对多和反向查询。在序列化器里访问外键对象时Django会延迟加载如果没有在get_queryset提前准备每一条记录都会多出一次查询。把这些工具用正确之后接口响应降到120ms已经远超普通校内系统的可用性标准。5. 给同样要做教务系统的朋友几句实在话做这个项目最大的收获是技术选型从来不是选“最强的”而是选“最匹配的”。学生信息管理系统是一个重度业务型项目开发周期短、权限关系复杂、内部管理需求强Django的ORM、Admin、DRF正好打中这几个要害。你问我换回SpringBoot能不能做当然能做但需要投入更多时间去配置基础设施。对于一个校内系统来说这些时间成本完全没必要。如果你也准备做类似系统我有几个建议数据模型动工前先找教务人员聊清楚“一个学生能选几门课”“成绩修改是否需要审批”“转班之后历史成绩怎么算”。这些业务规则直接影响模型设计后改成本很高。不要把Admin后台当成只能充数的玩具它完全可以成为正式的管理工具。配合list_editable和自定义action普通老师也能高效率地维护基础数据省下大量前端表格页面开发量。前端Vue3和DRF联调时尽量不使用复杂通信协议单纯用RESTful风格把每个资源都搞成/api/xxx/的标准接口字段命名统一用下划线风格前端再用工具转换省心也稳定。最后记得给数据库做每日备份。学生成绩和学籍信息都是不能丢的资产万一服务器磁盘损坏有备份和没备份是两个世界。我在这个项目里还留了一个扩展点后续可以把课程评价、请假审批也加进来模型层已经留好外键关系只需要新增一个App即可。这也是Django模块化的好处之一业务边界清晰App之间互不污染。希望这篇记录能帮到你少走一点弯路的经验比任何框架模板都值钱。
阅读完成 · 觉得有帮助?