每年三四月份,各高校计算机相关专业的毕设选题表一挂出来,"教务信息管理系统的设计与实现"这一行总是被划得最快。原因不复杂:业务场景人人都熟,需求张口就能说上几句,指导老师也不会追问你行业背景。但真动手做过的人清楚,这个题目属于典型的"上手容易、做好极难"——它最终能做成什么样,完全取决于你有没有想清楚教务业务里那几个绕不开的硬骨头:排课时间冲突、选课并发超卖、学籍与成绩数据的关联一致性。
我前后帮人看过十几份这个题目的代码和论文,技术栈从最早的 JSP + Servlet 三层结构,到后来的 SSM,再到现在主流的 SpringBoot + Vue 前后端分离。做得扎实的和做得敷衍的,差距往往不在界面漂不漂亮,而在于几个关键的业务模型有没有抽象对。这篇就把这些年自己踩过的、看别人踩过的坑整理一遍,从需求边界、技术选型、数据库建模,一路讲到排课算法和选课并发处理,按"能做出来、能答辩、能讲清楚"的标准来写,通用的设计思路放在前面,具体实现放在后面,做其他后台管理系统也能借鉴。
1. 教务系统到底要做成什么样:先把需求边界划清楚
接手这个题目的第一步不是打开 IDE 建工程,而是拿张纸把"谁在用、用它干什么"列出来。我见过太多人一上来就写代码,写到一半发现用户角色没分清,学生端和管理端的功能混在一起,最后返工重来。教务信息管理系统的角色划分其实非常固定:学生、教师、教务管理员这三类,偶尔会加一个"院系管理员"做二级审批,但毕设阶段一般用不上。
1.1 三类角色各自的核心诉求
学生的诉求集中在"查"和"选"两件事上:查自己的课表、查成绩、查学分修读进度;选课、退课、评教。这里有个容易被忽略的细节——学分修读进度这个功能看起来简单,实际上要求学生能清楚看到"培养方案要求多少学分、已修多少、还差哪一类"。分类别的学分统计(必修、选修、通识、实践)比单纯算个总数更能体现你对业务的理解,答辩时也是个加分点。
教师的诉求是"我的课"和"我的人":查看本学期授课任务、导出学生名单、录入和修改成绩、查看课程评教结果。成绩录入这块要注意,教师只能看到自己教的班级的学生,这个权限边界必须做死,否则就是安全问题。
教务管理员的活最杂,也最能拉开设计差距:用户管理(导入新生名单、重置密码)、课程库维护、开课计划制定、排课、选课轮次配置、成绩审核、统计报表。其中排课和选课轮次配置是两个技术含量最高的模块,后面会单独展开讲。
1.2 哪些功能是"必须有",哪些是"锦上添花"
做毕设最怕的就是贪多。功能列表拉出三四十条,最后每条都只做了一半,演示的时候处处是坑。我的建议是按下面的优先级来排:
| 优先级 | 功能模块 | 说明 |
|---|---|---|
| 必须有 | 登录认证、角色权限 | 系统的地基,做不好其他都是空中楼阁 |
| 必须有 | 课程库、开课计划管理 | 数据的源头,没有它后面的选课无从谈起 |
| 必须有 | 排课与课表查询 | 核心算法区,答辩重点 |
| 必须有 | 选课/退课 | 涉及并发的关键模块 |
| 必须有 | 成绩录入与查询 | 业务闭环 |
| 加分项 | 学分修读进度统计 | 体现业务理解深度 |
| 加分项 | 评教问卷 | 数据结构简单,但让系统更完整 |
| 加分项 | 数据看板/可视化 | 视觉冲击强,答辩时很讨喜 |
| 可放弃 | 消息推送、在线聊天 | 与核心业务关系不大 |
这张表的价值在于:当你时间不够时,知道该砍哪一刀。很多同学卡在评教模块的问卷动态渲染上,其实那部分完全可以做成固定几道题的静态表单,效果一样,但省下至少三天。
1.3 一个常被忽略的需求点:学期与选课轮次
教务系统里有个隐含概念叫学期(Term),所有数据几乎都要挂在某个学期下面。课程库是跨学期共享的,但开课、选课、成绩都是按学期隔离的。如果你在表设计时没有预留学期字段,后期想加"查看上学期成绩"这类功能时就得大改。
选课轮次(Round)也是同理:真实的教务系统会分"预选、正选、补退选"多轮,每轮的时间窗口、可选课程范围、容量规则都不一样。毕设做个简化版,只保留"开始时间、结束时间、状态"三个字段的轮次表就够了,但这个概念一定要有,否则选课功能会显得很单薄。
2. 技术选型:从答辩和交付倒推,而不是从流行度倒推
选技术栈这件事,很多人是反过来的——先看别人用什么,或者追最新的框架,结果踩了一路坑。毕设的技术选型应该遵循一个朴素的原则:资料多、坑少、能跑起来、能讲明白。你要的不是生产级的极致性能,而是在有限时间内交付一个逻辑自洽、演示流畅、答辩时经得起追问的系统。
2.1 三套主流方案的横向对比
我把这些年见过的主流组合整理成一张表,方便你按自己的基础对号入座:
| 方案 | 技术栈 | 上手难度 | 适合人群 | 主要风险 |
|---|---|---|---|---|
| A | SpringBoot + MyBatis + Vue3 | 中等 | 有 Java 基础,想做主流企业级项目 | 前后端分离的跨域、鉴权配置容易卡住 |
| B | Django + DRF + 模板/Vue | 较低 | Python 基础好,想快速出成果 | 自动生成的 ORM 迁移容易失控 |
| C | SSM 传统架构 + JSP/Thymeleaf | 较高 | 学校要求传统架构 | 前后端耦合,页面调试痛苦 |
如果没有任何偏好,我建议选 A。原因不是它"先进",而是它的报错信息最透明、社区问答最丰富。你在配置拦截器、处理跨域、写分页查询时遇到的 90% 问题,搜索引擎里都有现成答案。而 Django 虽然开发快,但一旦 ORM 关联查询写得不对,报出来的 SQL 往往让人摸不着头脑,对新手排查不友好。
2.2 后端框架的四个关键决策点
确定用 SpringBoot 之后,还有几个具体选择需要提前定下来,免得写到一半才发现不合适。
持久层用 MyBatis 还是 MyBatis-Plus。纯 MyBatis 需要手写所有 SQL 和映射文件,工作量大但控制精细,适合答辩时展示你对 SQL 的理解。MyBatis-Plus 提供了大量单表操作的封装,能省一半以上的代码量,多表关联查询再自己写 XML。我的建议是混合用:单表 CRUD 走 Plus,复杂的选课统计、成绩汇总自己写 SQL。这样既省时间,又有能拿得出手的复杂查询可以讲。
权限框架选 Spring Security 还是 Shiro 还是自己写拦截器。Spring Security 功能全但配置复杂,学习曲线陡;Shiro 轻量好懂,但版本更新慢;自己写 JWT 拦截器最简单,也最容易讲清楚。毕设场景下我更推荐自己写一套基于 JWT + 拦截器的方案,因为面试官和答辩老师更想听的是你对认证流程的理解,而不是你会不会配 Spring Security 的过滤器链。核心逻辑其实就三步:登录签发 token、请求携带 token、拦截器校验并解析出用户身份。
返回结果统一封装。这个习惯一定要从第一天就养成。所有接口返回统一的{code, message, data}结构,前端只需要写一套统一处理逻辑。我见过太多人每个接口返回格式都不一样,前端请求代码写成一团乱麻,后期改一个字段要翻十几个文件。
全局异常处理。配一个@RestControllerAdvice,把业务异常、参数校验异常、系统异常统一兜住,返回友好提示。这个配置花不了半小时,但能让你的系统看起来专业一大截——不会因为一个空指针就把整个堆栈信息甩到用户脸上。
2.3 前端不必追求花哨,但要有记忆点
前端这块,如果时间紧张,直接用 Element Plus 或 Ant Design Vue 的组件库,配几个页面就能出效果。但有两个地方建议多花点心思,因为它们直接影响答辩观感:课表页面用网格布局渲染(这个视觉效果最好,也最能体现你处理二维数据结构的能力),首页加一个数据看板(选课人数top10、各院系课程分布之类的图表,用 ECharts 半小时能搞定)。
跨域问题要提前解决,开发阶段在后端配一个 CORS 全局配置就行,别等到联调时才发现请求全被拦了。
3. 数据库建模:课程与开课分开,是这套系统的分水岭
数据库设计是这个题目最能体现水平的地方。我敢说,把"课程"和"开课"这两个概念混为一谈的人,后面一定会返工。这不是危言耸听,而是教务业务本身的结构决定的。
3.1 为什么课程和开课必须拆开
想象一下"高等数学"这门课。它有自己的属性:课程代码、课程名称、学分、总学时、课程性质(必修/选修)、开课院系。这些属性是长期稳定的,跨学期不变。但如果只有一个"课程表",你把它塞进这些字段,那问题就来了——2024 年秋季学期,高等数学由张老师教,5 个班,每班 60 人,周三 1-2 节;2025 年春季学期,同样这门高等数学,换成李老师教,3 个班,每班 50 人,周二 3-4 节。
这些每学期都在变的信息(任课教师、班级、人数、上课时间、教室),如果全都塞进课程表,那就意味着每学期都要为同一门课新建一条记录,课程代码就重复了,统计"高等数学历年选课人数趋势"这种需求直接没法做。
正确的做法是拆成两张表:**课程库(course)**存课程本身的静态属性,**开课表/教学班(teaching_class)**存某学期某门课的具体开设信息,用外键关联到课程库。这个设计一旦立住,后面的选课、成绩、统计全部水到渠成。答辩时如果老师问"你的系统怎么支持同一门课多个老师开课",你能脱口而出这套结构,分数就不会低。
3.2 几张核心表的字段设计
下面给出最核心的几张表的结构,字段是精简过的,实际可以按需扩展:
-- 用户表(学生和教师共用,用 role 区分,也可拆成两张表) CREATE TABLE `sys_user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '学号/工号', `password` VARCHAR(100) NOT NULL COMMENT '加盐后的密码', `real_name` VARCHAR(50) NOT NULL, `role` TINYINT NOT NULL COMMENT '1学生 2教师 3管理员', `college_id` BIGINT COMMENT '所属院系', `class_id` BIGINT COMMENT '所属班级,仅学生有', `status` TINYINT DEFAULT 1 COMMENT '1正常 0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 课程库(静态属性) CREATE TABLE `course` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `course_code` VARCHAR(20) NOT NULL UNIQUE, `course_name` VARCHAR(100) NOT NULL, `credit` DECIMAL(3,1) NOT NULL COMMENT '学分', `hours` INT NOT NULL COMMENT '总学时', `course_type` TINYINT COMMENT '1必修 2选修 3通识 4实践', `college_id` BIGINT COMMENT '开课院系' ); -- 开课表/教学班(每学期动态生成) CREATE TABLE `teaching_class` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `course_id` BIGINT NOT NULL, `term` VARCHAR(20) NOT NULL COMMENT '如 2025-2026-1', `teacher_id` BIGINT NOT NULL, `capacity` INT NOT NULL COMMENT '容量', `selected_count` INT DEFAULT 0 COMMENT '已选人数', `class_time` VARCHAR(50) COMMENT '上课时间,如 3-1-2,3-3-4', `classroom` VARCHAR(50), `status` TINYINT DEFAULT 1 COMMENT '1可选 0停开' ); -- 选课记录表 CREATE TABLE `course_selection` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `student_id` BIGINT NOT NULL, `teaching_class_id` BIGINT NOT NULL, `select_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `status` TINYINT DEFAULT 1 COMMENT '1已选 0已退', UNIQUE KEY `uk_stu_class` (`student_id`, `teaching_class_id`) );这里有几个设计细节值得单独说。选课表的唯一索引是必须的,它能在数据库层面兜住"同一学生重复选同一门课"的问题,哪怕你的应用层校验因为并发漏掉了,数据库也会拦下。开课表里的selected_count是冗余字段,正常应该实时count选课表,但选课高峰期每次查询都去 count 一次会很慢,所以用冗余字段 + 事务保证一致性是合理的取舍,答辩时你可以主动讲这个取舍,说明你懂范式和性能的平衡。
3.3 成绩表和学分统计的处理
成绩表设计相对简单,关键是什么时候写入。有两种思路:一是选课记录表里直接加成绩字段,二是单独建成绩表。我推荐单独建表,因为成绩录入有时间窗口,和选课是两个独立阶段,混在一起会让状态管理变复杂。
学分统计不建议存成表,而是实时计算 + 缓存。学生的学分修读情况需要按课程性质分组求和:
SELECT c.course_type, SUM(c.credit) AS total_credit FROM course_selection cs JOIN teaching_class tc ON cs.teaching_class_id = tc.id JOIN course c ON tc.course_id = c.id LEFT JOIN score s ON s.selection_id = cs.id WHERE cs.student_id = ? AND cs.status = 1 AND s.score >= 60 GROUP BY c.course_type;注意这里的s.score >= 60条件——只有及格才计入已修学分,这个细节很多人会漏,答辩时被问到会很尴尬。
4. 排课与时间冲突检测:位图编码比你想的好用
排课是整个系统里最有"算法味"的模块,也是答辩老师最可能深挖的地方。它的核心问题就一句话:在有限的时间和教室资源下,把课程安排得互不冲突。听起来简单,但冲突检测要同时考虑教师、班级、教室三个维度,还要处理教室容量、连堂课时等约束。
4.1 用位图表示时间,冲突检测变成位运算
一个非常实用的技巧是把一周的时间片编码成一个整数位图。假设我们把一周分成 5 个工作日,每天 5 个大节(上午 1-2 节算第 1 大节,以此类推),那么一周总共 25 个时间片。可以用一个 int 的低 25 位来表示某个对象(教师、班级或教室)的占用情况,第 n 位为 1 表示第 n 个时间片被占用。
这样冲突检测就变成了极简的位运算:
// 计算两个时间片集合是否有交集:与运算结果不为 0 就有冲突 public boolean hasConflict(int busyA, int busyB) { return (busyA & busyB) != 0; } // 合并占用:或运算 public int merge(int busyA, int busyB) { return busyA | busyB; } // 把 "3-1-2" 这样的字符串转成位图 // 3 表示周三,1-2 表示第 1 大节 public int parseToBitmask(String classTime) { int mask = 0; for (String seg : classTime.split(",")) { String[] parts = seg.split("-"); int day = Integer.parseInt(parts[0]); // 1-5 int startPeriod = Integer.parseInt(parts[1]); int endPeriod = Integer.parseInt(parts[2]); for (int p = startPeriod; p <= endPeriod; p++) { int index = (day - 1) * 5 + (p - 1); // 0-24 mask |= (1 << index); } } return mask; }这个设计的好处在于速度极快且逻辑清晰。手动排课时,系统把候选课程的位图拿出来,和教师、班级、教室已占用的位图依次做与运算,只要有任何一个不为 0,就说明冲突,直接在前端把冲突的具体原因和冲突对象回显给管理员。整个过程是 O(1) 的位运算,比循环比对时间区间快得多,也更容易写对。
4.2 三个维度的冲突要分别维护三张占用表
位图只是数据表示,真正要落库的是三张占用记录表:teacher_busy、class_busy、classroom_busy,每张表记录"某个对象在某学期的位图值"。每当有一次成功的排课,就要原子性地更新这三张表。这里必须用事务包起来,否则一旦中途失败,数据就不一致了,后续的冲突检测会全乱套。
@Transactional public void schedule(Long teachingClassId, Long classroomId) { TeachingClass tc = getById(teachingClassId); int mask = parseToBitmask(tc.getClassTime()); // 依次检查并占用三个维度 checkAndOccupy(teacherBusyService, tc.getTeacherId(), mask, tc.getTerm()); checkAndOccupy(classBusyService, tc.getClassId(), mask, tc.getTerm()); checkAndOccupy(classroomBusyService, classroomId, mask, tc.getTerm()); // 更新开课表的教室 tc.setClassroom(...); updateById(tc); }4.3 自动排课的思路:别追求最优,追求可用
如果要做自动排课,我的强烈建议是不要试图做全局最优解。真实教务系统的自动排课本质上是约束满足问题,理论上可以用遗传算法、模拟退火去做,但实现复杂度极高,而且很难调试。毕设阶段的可行做法是贪心 + 回退:按班级优先级排序,逐个尝试给每门课找可用的"时间片 + 教室"组合,找到就占用,找不到就记录下来标记为"待人工处理"。
这样做的结果是——系统能自动排掉 80% 的课,剩下的 20% 交给管理员手动调整。这个"人工兜底"的设计恰恰是真实系统的做法,也方便你在论文里讨论算法的局限性,显得思考全面。
提示:自动排课功能如果时间紧张,可以只做"冲突提示"不做"自动安排"。手动排课时实时检测冲突并给出候选的可用时间,实用性反而更高,风险也更小。
5. 选课并发:超卖问题必须在设计阶段解决
选课是这个系统里唯一可能出现真实并发的场景,也是最容易在答辩时被问到"如果一千个人同时抢一门课会怎样"的地方。常见的第一版实现是这样写的:先查询selected_count是否小于capacity,如果小于就插入选课记录并让selected_count + 1。这段代码在单线程下完全正确,但在并发下会超卖——两个请求同时读到selected_count = 99(容量 100),都判断为可选,结果都插入成功,最终选课人数变成了 101。
5.1 三种解决方案的对比
| 方案 | 实现方式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| 悲观锁 | SELECT ... FOR UPDATE锁行 | 逻辑简单,绝对安全 | 高并发下大量请求排队,性能差 | 一般 |
| 乐观锁 | 加 version 字段,更新时校验 | 无锁竞争,性能好 | 冲突多时重试频繁 | 推荐 |
| 唯一索引 + 原子更新 | 靠数据库约束兜底 | 最可靠,防重复也防超卖 | 需处理失败提示 | 强烈推荐 |
5.2 推荐的做法:一条原子 SQL 解决
最优雅的实现是把判断和更新合并成一条 SQL,利用数据库的行锁和原子性保证正确性,然后用返回的影响行数判断是否成功:
UPDATE teaching_class SET selected_count = selected_count + 1 WHERE id = #{classId} AND selected_count < capacity AND status = 1;这条 SQL 执行后,如果影响行数是 1,说明抢占成功;如果是 0,说明要么容量满了,要么课程停开了,直接给用户返回"选课失败,容量已满"。判断和自增在同一个语句里完成,中间没有其他事务能插进来,从根本上杜绝了超卖。
再把选课记录的插入放在同一个事务里,配合选课表的唯一索引,就能同时解决"超卖"和"重复选课"两个问题:
@Transactional public Result selectCourse(Long studentId, Long classId) { // 1. 原子占位,失败即容量满 int affected = teachingClassMapper.tryOccupy(classId); if (affected == 0) { return Result.fail("选课失败,该课程已满或已停开"); } try { // 2. 插入选课记录,唯一索引兜底防重复 selectionMapper.insert(studentId, classId); } catch (DuplicateKeyException e) { // 3. 重复选课时要把占的位子还回去 teachingClassMapper.release(classId); return Result.fail("你已经选过这门课了"); } return Result.ok(); }注意第三步的回滚占位,这是个容易被漏掉的细节。如果唯一索引拦下了重复插入,前面已经自增的selected_count必须减回去,否则会出现"人数虚高"的诡异现象——明明没几个人选,容量却显示满了。
5.3 退课同样要小心
退课看着比选课简单,其实也有坑。如果退课只更新选课记录的状态而不减selected_count,那这门课的容量就永远释放不出来了。正确的做法是先更新选课记录状态,再原子减计数,并且用选课记录的状态做幂等判断,避免重复点退课按钮导致计数被减两次。
-- 先判断这条记录是不是"已选"状态,避免重复退课 UPDATE course_selection SET status = 0 WHERE student_id = ? AND teaching_class_id = ? AND status = 1; -- 影响行数为 1 才执行下面的减计数 UPDATE teaching_class SET selected_count = selected_count - 1 WHERE id = ? AND selected_count > 0;AND selected_count > 0这个条件是为了兜底,防止数据异常时把计数减成负数。
6. 权限模型与数据隔离:越权是答辩的高频提问点
功能做完之后,很多人的系统其实是不设防的——只要浏览器里手工改一下 URL 里的 id,就能查到别人的成绩。这在答辩时如果被老师点破,会非常难堪。数据隔离这件事,做好了不显眼,做不好就是硬伤。
6.1 权限控制的三个层次
我认为一定要区分清楚三个不同的层次,很多系统的漏洞就出在只做了第一层:
- 认证层:你是谁。靠登录 + JWT 实现,拦截器校验 token 有效性,解析出用户 id 和角色。
- 授权层:你能访问哪些接口。靠角色判断实现,比如"录入成绩"接口只有教师角色的 token 才能调用。
- 数据层:你能看到哪些数据。这是最容易被忽略的一层。即使教师角色能调成绩录入接口,他也只能录入自己教的班级的学生成绩,不能通过改参数去改别人的。
6.2 数据层隔离的具体实现
数据层的隔离不能靠前端传参,必须从 token 里解析出身份,再去数据库验证归属关系。比如教师查学生名单,正确的逻辑是:
public List<Student> getStudentList(Long teachingClassId, Long currentTeacherId) { TeachingClass tc = teachingClassMapper.selectById(teachingClassId); // 关键一步:验证这个教学班是不是当前教师教的 if (!tc.getTeacherId().equals(currentTeacherId)) { throw new BusinessException("无权查看该班级"); } return selectionMapper.listStudents(teachingClassId); }学生查成绩同理,studentId必须从 token 里取,永远不要从请求参数里取。我见过最典型的漏洞就是接口写成GET /score?studentId=123,改个参数就能看别人的成绩。正确做法是接口根本不接收 studentId 参数,直接从当前登录用户上下文里拿。
这类"越权"问题在答辩中出现的频率很高,因为老师往往会现场让你演示一下,你打开浏览器开发者工具改个 id,如果数据还能正常返回,那就解释不清了。反过来,如果你主动展示"我改了参数,系统返回了无权限提示",这就是一个漂亮的加分项。
6.3 密码存储这件小事
顺带提一句密码。千万不要明文存数据库,也不要用简单的 MD5。至少要用 BCrypt 这类带盐的哈希算法,Spring Security 里有现成的BCryptPasswordEncoder可以直接用,哪怕你不用 Spring Security 的整套权限体系,单独引入这个工具类也值得。同一个密码每次加密结果都不同,但校验时又能正确匹配,实现起来就一行代码,性价比极高。
7. 那些文档里不会写、但一定会遇到的坑
前面讲的都是设计层面的东西,最后分享几个我在实际开发和调试中反复踩到的具体问题,属于"书上不会写、但一写就中招"的类型。
7.1 中文与字符编码问题
从 Excel 导入学生名单、导出成绩单这两个功能,几乎必然会碰编码问题。现象是导入的姓名变成乱码,或者导出的 CSV 用 Excel 打开全是问号。原因是 Excel 默认按 GBK 解析 CSV,而你的程序用的是 UTF-8。解决办法是在导出的 CSV 文件开头写入 BOM 头(\uFEFF),Excel 就能正确识别 UTF-8。导入时则要提示用户把 Excel 另存为 CSV UTF-8 格式。这类问题查起来很折腾,但解决方案就是一行代码的事,早知道早省心。
7.2 时间格式的二义性
class_time这种自定义字符串格式(比如3-1-2)在解析时很容易出错,尤其是天数超过 9、大节超过 9 的情况。我建议在存储时就约定死格式,并且在项目里写一个专门的工具类统一处理解析和格式化,所有地方都调这个工具类,绝不允许各处自己split("-")。这样一旦格式要调整,只改一个地方。
7.3 分页查询的排序稳定性
选课记录列表、成绩列表这些地方,如果ORDER BY的字段有重复值(比如好多学生的选课时间是同一秒),翻页时会出现"某条记录在第 1 页出现过,翻到第 2 页又出现"的现象。解决办法是排序字段后必须再跟一个唯一的 id 字段,比如ORDER BY select_time DESC, id DESC,保证排序的确定性。
7.4 事务失效的三种常见写法
明明加了@Transactional事务却不生效,这也是高频问题。常见原因有三个:方法不是 public 的;在同一个类里用this.调用了另一个带事务的方法(绕过代理);异常被 catch 了没有重新抛出。最稳妥的经验是:事务方法一律 public,跨方法调用一律走注入的 Bean,捕获异常后要记得throw出去或手动回滚。
7.5 演示数据的准备
最后一条是经验之谈,但非常重要:答辩前几天一定要准备一套像样的演示数据。一个只有三五条课程记录、学生姓名叫"张三李四王五"的系统,和一个有真实院系名称、几十门课程、上百条选课记录的系统,给人的观感完全不同。花半天时间用脚本批量生成一批结构合理、逻辑自洽的数据(比如让学分分布符合正常规律、选课人数有高有低),演示效果会提升一个档次。这套数据还可以顺便用来做压力测试,验证你的选课模块在几百条并发下是否真的没有超卖。
我在带人做这个题目的过程中最深的体会是:教务信息管理系统真正的价值不在于实现了多少功能,而在于你有没有把几个核心的业务模型抽象对。课程和开课分开、时间用位图表示、选课靠数据库原子操作防超卖、数据隔离从 token 取身份,这四件事做对了,系统就立住了,功能多少反而是次要的。反过来说,如果这几个地方是糊涂的,功能堆得再多,答辩时几轮追问下来也会露馅。所以如果你现在正对着这个题目发愁,我的建议是先把上面这几块想透,把表结构画在纸上推演一遍业务流程,再动手敲代码,能省下后面大量的返工时间。