如果你正在为课程设计或毕业设计选题发愁,或者已经选了“教务系统”这个方向却不知道怎么把学生信息、课程安排、成绩查询、选课这些功能落成一套可运行的代码,那这篇内容应该能帮到你。我用Java+SSM和Python+Django各实现了一套完整的教务信息平台,从数据库设计到核心业务逻辑再到部署调试,都整理成了可以直接参考的源码和配套文档。这套项目包含学生信息管理、课程安排、成绩查询、教学管理、学籍管理、考试安排、选课系统等核心模块,既覆盖了教务业务的主要场景,也兼顾了两种主流Web技术栈的工程量。无论你是Java方向还是Python方向,或者两个方向都有所涉及,这套双栈实现都能当作用一个业务需求来对比理解框架设计差异的活教材。
1. 项目整体设计与技术选型
1.1 这套系统的定位与目标用户
教务信息平台是个非常经典的业务系统选题,因为它包含了业务管理系统里几乎所有常见场景:用户认证与权限区分、基础数据维护、多条件查询与分页、外键关联和事务处理、甚至还有一点简单的排课约束逻辑。该说不说,选这个题目做课程设计或者毕业设计,性价比确实高:业务需求容易向评审老师讲清楚,功能边界又足够撑起一篇完整的系统设计文档。
这套项目的目标用户很清晰。一类是正在做毕业设计的本科生,需要跑通一个完整系统并写出对应的设计和说明文档;另一类是刚学完SSM或Django基础、想通过一个完整项目把框架知识串起来的自学开发者。我不会把内容写得太教科书化,更多的是一步步把“这个项目到底怎么做出来”的细节讲透。哪怕你没亲手写过SSM的XML配置,或者对Django的ORM只停留在会用objects.filter的程度,照着这套代码思路往下走,也能在几天内把系统跑起来,并且能说出每个模块背后的设计原因。
1.2 为什么选“SSM打底 + Django翻版”这套组合
很多同学会问,为什么一个教务系统要做两套技术栈?不是增加工作量吗?这里我得说清楚:这个项目的初衷不是追求“最快的开发效率”,而是要同时覆盖Java和Python两个方向的招聘和技术热门方向。Java后端和Python后端在就业市场上都是主流选择,很多人在两者之间纠结。与其纠结,不如用一个相同业务需求,分别用SSM和Django各实现一遍,在对比中自然就理解了框架的设计取舍。
从技术难度和框架学习曲线来看,SSM是Java系里最经典的组合:Spring管对象和事务、SpringMVC管请求分发、MyBatis管数据库操作。这套组合的灵活性高,但配置相对繁琐,适合用来理解Web框架的底层协作方式。而Django走的是“全家桶”路线,自带ORM、Admin后台、认证体系和模板引擎,开发效率高,适合快速搭建业务原型。同一个教务平台用这两套实现,能很直观地对比出“SSM中需要手动配置的东西,Django里到底替你做了什么”。
从最终交付的角度看,源码加调试文档加讲解的配套形式,也方便在答辩时展示。你可以先讲Java版的SSM实现,突出事务管理、Mapper接口和SpringMVC请求流;再切到Django版,演示Admin后台几行代码就生成增删改查界面,顺便讲透两套方案的适用场景。这样整个答辩逻辑是自洽的:我不仅会写代码,还知道技术方案何时该选哪种。
1.3 功能范围与页面结构速览
我整理了一下这套系统覆盖的功能清单,包含六个主体模块:
- 学生信息管理:学生档案的增删改查、按学号/姓名/班级/专业/入学年份条件组合查询
- 课程安排:课程基础信息维护、授课教师关联、上课时间地点设置
- 成绩管理:平时成绩和期末成绩录入、加权总评计算、按课程和学生维度查询成绩
- 教学管理:教师授课任务分配、教学班管理、教师信息的维护
- 学籍管理:学生入学、休学、复学、毕业等状态管理
- 考试安排:考试时间地点分配、监考教师安排
- 选课系统:学生自主选课、时间冲突检测、退课操作
页面结构上,两种技术栈都采用传统的服务端渲染模式。Java端用JSP,借Bootstrap做页面样式;Django端用模板继承和自带模板语法。前台用户界面和学生登录界面分开,后台管理只对教师和管理员角色开放。菜单层级不超过三层,保证操作路径短,也方便在文档里画功能结构图。
2. SSM侧核心设计细节
2.1 Controller / Service / Mapper 三层是怎么分工的
SSM项目的代码组织方式基本都遵循三层架构,这也是Java面试题里反复被拷问的点。在教务系统里,我把三层职责拆得非常明确:
Controller层只接收请求、解析参数、调用Service、返回视图或JSON数据。比如成绩查询接口,GradeController负责从HttpServletRequest里拿到当前页、每页条数、课程ID这些参数,然后交给Service层,最后把分页结果塞进ModelAndView。Controller里不出现任何SQL和业务判断,尽量保持“薄”。
Service层承载全部业务逻辑,包括事务控制、数据校验、冲突检测。比如选课时的冲突检测,就在CourseSelectionService里完成:先查出该学生已选的所有课程时间段,再和目标课程逐条比对,如果发生重叠就抛业务异常,由全局异常处理器转成页面提示。
Mapper层只做数据访问。MyBatis的Mapper接口定义方法,XML文件里写SQL。这里有个经验:多表关联查询不要全堆在主Mapper里,比如成绩列表需要关联课程名和学生名,我建议单独建一个GradeCustomMapper,只负责组装复杂列表查询的SQL,这样基础的CRUD和报表类查询互不干扰。
分层的好处不只是好写文档,更重要的是调试的时候能快速定位问题。我踩过一次坑:成绩排名的SQL算出的结果是错的,页面展示却正常,最后定位发现是Controller里传参顺序和Service层接收顺序对不上,把班级ID和课程ID传反了。如果是分层清晰的结构,这种问题在Service入口加个日志就能立刻看出来。
2.2 MyBatis动态SQL与应用场景
教务系统的查询条件非常灵活,光是一个学生列表,就可能组合出十几个查询条件:学号模糊匹配、姓名模糊匹配、学院下拉筛选、专业下拉筛选、入学年份范围、性别选项。如果为每种组合写一条SQL,工作量会爆炸。MyBatis的动态SQL就是解决这个问题的核心技术。
以一个典型的多条件查询为例,我的学生列表Mapper大概是这样的:
<select id="findByCondition" resultMap="StudentResultMap"> SELECT s.*, c.class_name, m.major_name FROM tb_student s LEFT JOIN tb_class c ON s.class_id = c.id LEFT JOIN tb_major m ON c.major_id = m.id <where> <if test="stuNo != null and stuNo != ''"> AND s.stu_no LIKE CONCAT('%', #{stuNo}, '%') </if> <if test="name != null and name != ''"> AND s.name LIKE CONCAT('%', #{name}, '%') </if> <if test="classId != null"> AND s.class_id = #{classId} </if> <if test="majorId != null"> AND c.major_id = #{majorId} </if> </where> ORDER BY s.enroll_date DESC </select>这里的核心要点是<where>标签会自动处理开头的AND,如果所有条件都为空,生成的SQL就是一个不带WHERE的全表查询,不会出现语法错误。<if>标签则按条件拼接片段。这种写法比在Java代码里拼SQL字符串安全得多,因为参数始终走预处理,能有效避免SQL注入风险。
我还建议学一下<foreach>标签,它在批量插入和批量删除场景里很常用。比如批量导入学生数据时,List参数通过foreach循环拼成多组VALUES,一次INSERT就能搞定,性能比单条循环插入高很多。需要注意的一个坑:foreach的集合类型在XML里要写清楚,如果是List参数,collection属性写list,如果是使用@Param注解命名的参数,就写注解名,这个写错了会直接报异常。
2.3 权限控制与Session处理
教务系统里至少有三种角色:学生、教师、管理员,不同的角色能看到的菜单和能执行的操作天差地别。学生能看自己的成绩和课表但不能改,教师能录成绩但不能改学生个人信息,管理员什么都行但也不能乱改成绩。我把权限控制放在了两个层面来做。
第一层是登录后的Session标记。用户登录成功后,把用户ID、姓名、角色代码存进Session。页面上的菜单选项根据角色代码动态渲染,管理员登录后才有“学籍管理”“考试安排”入口,学生登录后就没有“成绩录入”这个按钮。这层过滤做得简单直接,代码维护成本低,也方便在答辩时讲解。
第二层是SpringMVC的拦截器,用来保护URL级别的访问路径。我写了一个LoginInterceptor,注册到SpringMVC配置里,排除掉登录页、验证码、静态资源这些公开路径,其余所有业务URL都走拦截。同时有一个AdminInterceptor只过滤/admin/**路径,用于校验管理员权限。这样即使有人猜到了某个管理接口的URL,不登录也进不去。
权限控制的延伸设计里,还涉及一个刚学的@Transactional注解的使用。比如教师录入成绩时,需要先检查该教师是否确实教这门课,再写入成绩表,同时更新该学生的总评数据。这个流程里两步操作必须同时成功或同时失败,所以我在Service方法上加@Transactional(rollbackFor = Exception.class),配合Spring的声明式事务管理。如果不加事务,可能成绩明细写进去了,总评更新失败,数据就出现不一致。这一点在文档里的“功能测试用例”部分值得重点强调一下。
3. Django侧核心设计细节
3.1 MTV结构在教务场景里的映射
Django的项目组织方式和SSM差别挺大,它是按app来划分模块的,每个功能域独立成一个app。这个教务系统我把app按业务拆成了五个:users管登录和用户信息、students管学生档案和学籍、courses管课程和排课、grades管成绩与统计、selects管选课和冲突检查。每个app都有自己的models、views、urls、admin,职责边界比Java端的包结构还要直观。
写Django工程量最大的其实不是业务代码,而是理解“请求到底是怎么路由到视图函数的”。我刚开始用Django时经常搞混path和re_path,后来才总结出规律:对一个REST风格的URL比如/student/2021001/,path里用转换器来捕获变量,path('student/<int:stu_id>/', views.student_detail);如果需要更灵活的正则匹配,就用re_path配合命名分组。一个容易犯的错是把<stu_id>和<int:stu_id>混写,后者带类型转换,Django会自动把参数转成int,而前者转出来是字符串,和后续数据库查询条件一比对就会出错。
模板层的设计我走的是“继承”路线:写一个base.html放导航栏、CSS和JS引用、用户登录信息展示,子模板用{% extends 'base.html' %}继承,只填充{% block content %}部分。这样改一次导航栏结构,所有页面同步更新,比SSM里每个JSP都要独立维护头尾要省心得多。对于列表分页,Django原生有Paginator类,在视图里分页、模板里渲染页码,再配合Bootstrap的分页样式,效果和Java端的PageHelper差不多。
3.2 ORM难点:多表关联和冲突查询
Django最吸引人的地方就是它的ORM。教务系统里最复杂的查询场景是:查一个学生选的所有课、同时显示课程的授课老师和上课时间地点。如果用原生SQL写要连三张表,但在Django ORM里只需要在模型上把外键关系定义好,然后利用双下划线跨表查询。
class CourseSelection(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='selections') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='selections') created_at = models.DateTimeField(auto_now_add=True) # 查询学生张三的所有选课及课程信息 selections = CourseSelection.objects.filter( student__name='张三' ).select_related('course__teacher')这里的student__name就是双下划线跨表过滤的经典写法,你可以一层一层往深处关联,course__teacher__name也能用。select_related则是在SQL层面用JOIN把关联对象一次性查出来,避免循环访问数据库。一个常见的性能坑:在模板里循环展示选课时,如果没有select_related,每显示一行课程信息都会额外执行一次查询,几十条记录就是几十条SQL,Django调试工具里能明显看到查询次数暴涨。
选课冲突检测是另一个有代表性的ORM用法。在Java端我用Java代码判断时间段重叠,在Django端可以直接用ORM构造起止区间交叉查询:
conflicts = Course.objects.filter( schedules__day_of_week=target_schedule.day_of_week, schedules__start_section__lt=target_schedule.end_section, schedules__end_section__gt=target_schedule.start_section, selections__student=current_student )这段逻辑表达的是“两个时间段存在重叠区域的充要条件是一方开始时间小于另一方结束时间、且一方结束时间大于另一方开始时间”,对于处理上课节次、会议室预订这类重叠判断,这是个可以反复使用的套路。
3.3 Admin后台与权限管理
Django自带一个Admin后台,这是它相比Java系框架的一个巨大优势。我在models里注册了全部业务表之后,再在admin.py里配置列表显示字段和搜索字段,比如:
@admin.register(Course) class CourseAdmin(admin.ModelAdmin): list_display = ['course_code', 'course_name', 'credit', 'teacher', 'course_type'] search_fields = ['course_code', 'course_name'] list_filter = ['course_type']系统运行起来后,访问/admin/就能直接对课程、学生、班级这些基础数据进行可视化维护。用Admin后台来完成“基础数据初始化”非常方便,比在Java端一点点敲SQL省事得多。但要注意,Admin后台最好只给管理员用,不能放给学生身份。我通过has_module_permission方法对某些模型做了管控,确保普通用户进入不了这些页面。
Django的权限体系自身就有三要素:User、Group、Permission。利用内置的@login_required装饰器能保护视图函数,利用@user_passes_test可以自定义校验函数,比如检查当前用户是否是教师角色。对于班级辅导员只能看本班学生的需求,我写了一个自定义校验:
def is_class_counselor(user): return user.is_authenticated and user.role == 'counselor'把它挂到学生列表视图上,非辅导员角色直接重定向到无权限页。这个方案比自定义装饰器简单,还能和Django自带的登录系统无缝配合。有一点要特别注意:Django默认开启了CSRF防护,所有POST表单模板里都要写{% csrf_token %},这一点和Java端SpringMVC默认不开CSRF是不同的,刚转过来时容易漏掉导致403。
4. 数据库设计与核心功能逻辑
4.1 十几张表怎么串起整个教务流程
教务系统的数据库设计是整个项目的地基,表结构如果设计不好,后面写业务代码会非常痛苦。我最终的库里有这么几张核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| tb_user | 用户账号 | username, password, role |
| tb_student | 学生档案 | stu_no, name, gender, class_id, enroll_date, status |
| tb_teacher | 教师信息 | name, title, dept_id, phone |
| tb_major | 专业 | major_name, dept_id |
| tb_class | 行政班级 | class_name, major_id, grade, counselor_id |
| tb_course | 课程 | course_code, course_name, credit, hours, course_type |
| tb_schedule | 排课表 | course_id, teacher_id, classroom, day_of_week, start_section, end_section |
| tb_selection | 选课记录 | student_id, course_id, created_at |
| tb_grade | 成绩记录 | student_id, course_id, regular_score, exam_score, total_score, semester |
| tb_exam | 考试安排 | course_id, exam_date, start_time, end_time, room, invigilator |
这十来张表的关系用一个类比来理解:tb_course是菜单,tb_schedule是餐厅的营业时间和座位安排,tb_selection是客户下的单,tb_grade是下单之后的口味评价。学生选课产生的记录在tb_selection,成绩表单独存,这样即使退课重新选,历史成绩也不会被覆盖。
设计成绩表时我特意把regular_score和exam_score分成两个字段,而不是只存一个总评。原因有二:一是教师录成绩时经常先录平时分后录期末分,分开存方便分阶段录入;二是后期如果要算“期末不及格但平时分很高的学生名单”,分开查直接有效。总评分total_score是一个冗余字段,由加权公式在Service层计算出来再写入,查询时不需每次现算。冗余字段在这个数据量级上完全可以接受,反而能显著提升成绩列表页的响应速度。
4.2 选课冲突检测的实现思路
选课系统里最有技术含量的功能就是冲突检测。我设置了一个上课时间段模型:一周7天,每天12节课,每节课有对应的节次编号。一门课由若干节次的排课记录组成,比如“周一34节、周四56节”。学生选课时要检查目标课的每个时间段是否和已选课程的时间段重叠。
冲突判断的逻辑我前文提到了区间交叉条件,这里补充设计的原因:为什么不比较“开始节次相等”这种简单条件?因为存在一门课是2节连上、另一门课是3节连上的情况,两个区间部分重叠,只比较头尾的等值条件会漏判。区间重叠判断则能覆盖所有场景。
Java端我选择先把学生已选课程的时间段查出来,在Service层遍历判断。这样代码逻辑直白,SQL简单,数据量小的时候性能也不差。如果要应对几千人同时抢课的并发场景,就得考虑数据库层面加锁或者用Redis做预占位,但这个体量的双技术栈课程设计项目,先保证逻辑正确更实际。Django端用ORM区间查询解决同一个问题,写出来的代码行数更少,适合快速实现。
4.3 成绩加权与绩点计算
成绩模块除了增删改查,还有一个藏在细节里的点:总评成绩的计算规则不是固定的。有的课程是“平时30% + 期末70%”,有的是“平时40% + 期末60%”,体育课甚至是“考勤20% + 项目40% + 期末40%”。如果把这个比例写在Service层代码里硬编码,换规则就得改代码。我建议把加权方案做成可配置,最简单的做法是在tb_course表里加两个字段:regular_ratio和exam_ratio,录入课程时直接定好权重。
计算总评时:
int totalScore = Math.round(regularScore * regularRatio + examScore * examRatio);Django版则写在模型的方法里:
def calculate_total(self): return round( self.regular_score * self.course.regular_ratio + self.exam_score * self.course.exam_ratio, 0 )这样课程的基础数据调整后,成绩自动按新规则计算,不会出现规则变化导致的历史数据不一致。绩点计算我按照常见的4.0制标准来做:90到100分对应4.0,85到89对应3.7,以此类推。这个逻辑也要注意,如果学校采用的是5.0制,只需要改grade_to_points()一个方法的映射表,其他统计代码不受影响。
4.4 排课与考试安排的约束处理
排课模块看起来简单,仔细想想其实是个小型的约束满足问题。教室不能在同一时间段被两门课占用,教师不能在同一时间段上两门课,班级也不能同一时间出现在两个不同的教室。在课程设计这个体量下,我不打算写一个智能排课算法,而是做成“排课冲突检测”。
在Java端录入排课时走一个校验流程:先按教室查该时间段的排课记录,再按教师查,再按班级查。只要某一项查出已有记录,就提示冲突,不让保存。这三步校验合在一起,基本能覆盖现实中“教室被占”、“老师赶场”、“学生撞课”三大问题。Django端我用了事务加select_for_update,对目标教室和教师的时间段记录加锁,防止两个管理员几乎同时录排课时互相覆盖。
考试安排则直接复用排课的时间段模型,把课程、教室、监考教师绑定到某一天的具体时间段。考虑一个实用性的限制条件:同一个监考教师不能在同一时间出现在两个考场。这块我做了同排课类似的冲突检查。再补一个容易被忽略的点:考试安排和平时上课时间冲突与否并不重要,因为期末停课后才安排考试,但同一个教室同一时段只能排一场考试是必须保证的。
5. 环境配置、启动步骤与调试实录
5.1 开发环境版本清单
基于我实测过程中反复踩坑的经验,先给你一张环境版本对照表,照着这个组合来配会省掉很多莫名其妙的Error:
| 组件 | 推荐版本 |
|---|---|
| JDK | 1.8(不要直接用17,老框架要么跑不起来要么要额外适配) |
| Maven | 3.6.x |
| Tomcat | 8.5.x |
| MySQL | 5.7或者8.0(8.0要注意驱动名和时区参数) |
| Spring | 5.2.x |
| MyBatis | 3.5.x |
| Python | 3.8 或 3.10 |
| Django | 3.2 LTS(如果需要新功能再用4.x) |
| 前端 | Bootstrap 4.x + jQuery |
强调一下:JDK版本是最容易掉坑的。如果你的电脑上装的是JDK 17,而项目用的Spring老版本还是基于Java 8编译,直接跑大概率会遇到非法字符或者类版本不支持的问题。给课程设计用的环境,稳妥比“用最新版”要重要得多。
5.2 启动Java工程的步骤和容易踩的坑
Java端项目拿到手之后,第一步不是直接跑Tomcat,而是先改数据库配置。找到jdbc.properties文件,把jdbc.url、jdbc.username、jdbc.password改成你本地MySQL的连接参数。MySQL 8.0要特别注意URL里带上时区参数:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/edu_admin?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码注意MySQL 8.0的驱动类换成了com.mysql.cj.jdbc.Driver,是带cj的,如果还在用旧的com.mysql.jdbc.Driver会直接报驱动类找不到或连接失败。另外驱动依赖也在pom.xml里检查一下,8.0.x对应的连接器版本不能太老。
然后执行项目里的db/edu_admin.sql脚本初始化表结构和测试数据。这里我建议直接用命令行source执行,比Navicat里复制粘贴更不容易漏语句。确认表建出来并且有测试数据后,在IDEA里把项目打成war包丢到Tomcat的webapps目录,或者直接在IDEA里配置好Tomcat通过catalina.bat run启动。
本地上跑起来后,打开浏览器访问http://localhost:8080/edu_admin/login,能看到登录页就说明SSM主链路通了。如果报404,优先检查web.xml里配置的dispatcherServlet的url-pattern是不是/,以及Maven依赖里有没有把war包插件配好。如果页面能打开但样式全丢,十有八九是项目里的静态资源路径问题,检查spring-mvc.xml里的<mvc:resources>映射或者模板里${pageContext.request.contextPath}是否漏写。
5.3 启动Django工程的步骤
Django端的启动比Java端省心得多。推荐先建一个虚拟环境,再安装依赖:
python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install -r requirements.txtrequirements.txt里的核心依赖就是Django、mysqlclient或pymysql、Pillow。如果用的MySQL,建议装pymysql然后在项目__init__.py里写入pymysql.install_as_MySQLdb(),能避免一堆编译问题。不要盲目装最新版Django,3.2、4.x、5.x在一些API上有细微差异,有时同一套代码在5.x里直接报错。
然后执行迁移:
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 8000先访问http://127.0.0.1:8000/admin/,用刚创建的超级用户登录,看看能不能进管理后台。如果登录页打开了但CSS样式没加载,多半是settings.py里的STATIC_URL和模板里加载静态资源的方式不匹配。解决方案是检查模板里是否写的{% load static %},以及static目录路径是否真的存在。如果manage.py migrate时报错django.db.utils.OperationalError,优先检查settings.py里数据库连接的用户名密码和数据库名是否正确。
5.4 造数据的小技巧
系统要演示得好看,光靠手工在页面上一条条加数据是不现实的。我建议在数据库层面直接造一批有规律的测试数据。
学生数据的学号可以用循环生成,比如一个班级30个人,学号从20210001到20210030,姓名随机生成。课程数据按公共课、专业课、选修课三种类型各造几门。成绩数据造的时候要注意:同一门课一个班几十个人的成绩最好大致符合正态分布的随机数,这样后期你写“成绩分布统计”功能时图表才会好看,全是满分或全是及格线附近的分数,看起来非常不真实。
Django端造数据可以直接写一个management/commands/generate_data.py的自定义命令,用Model.objects.create()批量生成,装好后执行python manage.py generate_data一次搞定。Java端没有这种便捷通道,就用SQL存储过程或一个初始化SQL脚本批量INSERT。脚本文件里最好每条INSERT语句后面带明确的取值范围注释,方便答辩时临时改数据量大小。
6. 常见问题与排查速查表
6.1 一套Bug实录:从404到500的排查路径
项目调试过程中遇到的各种报错,是最有价值的经验沉淀。我整理了一批高频问题,覆盖两套技术栈,下面这个速查表可以直接当排查手册用。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 启动Tomcat后访问8080端口直接404 | webapps里没有war包,或部署路径和URL不匹配 | 检查war包名称和访问路径是否一致 |
| JSP页面报The absolute uri错误 | JSTL标签库依赖缺失 | 在pom.xml里补上jstl和standard依赖 |
| MyBatis报Invalid bound statement | Mapper接口和XML的namespace或方法名对不上 | 逐个检查namespace和id是否匹配 |
| SpringMVC返回JSON乱码 | 消息转换器没配UTF-8 | 在spring-mvc.xml里配StringHttpMessageConverter |
| MySQL连接报Public Key Retrieval错误 | MySQL8.0的加密规则和驱动不匹配 | URL加allowPublicKeyRetrieval=true&useSSL=false |
| Django显示403 Forbidden | CSRF校验失败 | 检查表单里是否加了{% csrf_token %} |
| Django访问/admin页面样式丢失 | 没配置静态文件收集 | 模板确保有{% load static %},确认static目录路径正确 |
| 登录后Session失效跳回登录页 | Session超时或拦截器配置过滤了登录请求 | 检查拦截器的排除路径是否包含登录接口和静态资源 |
| 成绩总数对不上 | 选课记录和成绩记录存在孤儿数据 | 检查外键级联删除配置,退课时只删选课记录、不删成绩 |
| 分页页码点击后无数据 | 当前页参数丢失或SQL里LIMIT计算错误 | 断点检查页码参数传参,确认页容量和偏移量算法 |
6.2 数据库层面的独家避坑经验
数据库是这类系统最容易出问题但最容易被忽视的层面,有几个我自己实测下来很重要的经验。第一个是字符集问题:建库时一定要显式指定utf8mb4字符集,而不是默认的latin1,不然后期插入中文姓名或专业名称会变成乱码,甚至直接报“Incorrect string value”错误。初始化SQL脚本开头写清楚:
CREATE DATABASE edu_admin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE edu_admin;第二个是多表联查时的排序字段选择。排名类和统计类查询,我建议始终以主表ID加更新时间双字段排序,不要只按业务字段排序。比如成绩排名按总分倒序时,总分相同的情况下按学号升序,保证每次查询结果顺序稳定,否则分页场景下会出现“同一行记录出现在两页”的重复数据问题。
第三个是外键的级联策略。在学生和选课记录之间、课程和成绩记录之间,我建议全部使用限制删除(RESTRICT)而不是级联删除(CASCADE)。为什么?因为教务系统的数据是强审计敏感的。一个学生刚退课,管理员就把课程删掉了,结果成绩表里留下了一条指向不存在课程的记录,查询时LEFT JOIN会产生大量NULL字段。实际做法是删除课程前先检查有没有关联的选课或成绩数据,有就给提示,不直接删库。
6.3 两套框架开发体验上的差异总结
既然这个项目用两套技术栈实现了同样的业务,我最后再聊几句开发中的体感差异,这部分内容在答辩时用来回答“为什么做两套”这类问题很合适。
SSM的开发节奏是“配置驱动”。一个业务模块的完成包括:创建数据库表、写实体类、写Mapper接口和XML、写Service接口和实现类、写Controller、写JSP页面。每个环节都需要手动维护对应关系,写起来确实繁琐,但每一步都在加深对框架协作机制的理解。我强烈建议新手至少手动配一次完整的SSM项目,再使用各种快速脚手架,否则出了问题你可能完全找不到排查方向。
Django的开发节奏是“约定优于配置”。模型定义好后,数据库表、后台管理、表单校验甚至基础URL,框架都帮你生成了。MVC中的Controller被view函数和URLconf替代,ORM让大部分SQL不用手写。开发效率明显更高,一周不到能把功能原型写得七七八八。但要警惕所谓的“Django魔法”:框架替你做的事情越多,你越要主动了解它背后的实现。比如ORM延迟加载、QuerySet惰性求值,这些策略如果不了解,可能在数据量大时踩很深的性能坑。
这两套框架没有绝对的优劣,只有适不适合。教务系统这种管理型网站,Django确实开发起来更顺,但SSM能帮你把所有底层细节都看清楚。两者都做过一遍之后,再回头去看Spring Boot、MyBatis-Plus这些更现代的框架,理解速度会快很多。
最后再分享一点实际运行中的体会:把两套系统真正跑起来之后,你会发现学分制下最核心的痛其实是选课逻辑和成绩数据的联动。选课时间冲突检测能拦住一部分问题,但真正检验系统健壮性的,是“退课之后成绩表里的记录怎么办”“课程停开之后已选学生怎么处理”这样的边界场景。我把这些边界情况全部在文档里做了说明,也对应实现了提示逻辑,而不是简单地报个错就不管了。如果你的系统还要继续扩展,我建议优先考虑加入“教学评价”“培养方案管理”这两个模块,它们和现有的课程、成绩数据是天然联动的,扩展成本小,增量价值却很明显。