又到了每年毕设选题的季节,后台每天都会收到类似“Java毕设做什么题目好”“教务系统能不能做”“有没有带源码的参考项目”这类问题。说实话,高校教务系统确实是Java Web方向里经久不衰的题目,从十年前的JSP时代到现在的Spring Boot时代,它一直是答辩现场的常客。原因很简单:这个选题几乎覆盖了一个Java后端开发者需要掌握的全部基础能力——表结构设计、增删改查、权限控制、事务管理、文件上传导出,难度适中又不至于太单薄。这篇内容我准备把整个系统的设计思路、表结构、核心代码逻辑以及我在实际调试中踩过的坑一次性讲清楚,尤其适合正在准备毕设或者想拿这个项目练手的人参考。
先交代一下这个系统的定位:基于Java的高校教务管理系统,主要服务三类角色——管理员、教师、学生。管理员负责基础数据维护(院系、专业、教室、学期)、教师信息管理、学生信息管理、课程开设审核等;教师负责成绩录入、查看授课列表、管理教学班;学生负责选课、退课、查看成绩、查看课表。这套业务模型几乎是国内高校教务系统的标准缩影,做出来之后放到简历上,也能够比较完整地体现你对业务系统和权限模型的理解。
1. 选题定调:教务系统作为毕设课题,赢面在哪里
很多人纠结毕设到底选什么,选电商、选博客、选管理系统,总觉得教务系统太“老套”。但我这几年看过不少答辩现场,真正翻车的往往不是题目老,而是把题目做成了纯粹的CRUD流水账。教务系统这个题目的优势在于:它的业务规则足够复杂,天然带着权限模型、选课冲突检测、成绩计算这类有“含金量”的需求点,做好了反而比千篇一律的购物车更能展示设计能力。
1.1 系统价值不只是在“增删改查”
如果只把学生信息做几个增删改查页面,那确实没有任何竞争力。但高校教务系统真正的难点和亮点都隐藏在业务规则里:
- 选课模块需要处理课程容量、已选人数、选课时间窗口、禁止重复选课,这里涉及事务和唯一约束;
- 成绩模块需要考虑成绩组成(平时成绩、期末成绩、总评),以及成绩发布后的教师端修改限制;
- 权限模块需要区分学生、教师、管理员三类角色的菜单和操作权限;
- 课表模块需要处理周次、节次、教室冲突的判断逻辑。
这些规则一旦做扎实了,论文的核心章节也有东西可写,而不是只能贴一段CRUD代码凑字数。
1.2 适合谁来做这个课题
- 后端基础偏弱、想找一个中规中矩但能完整跑通的项目的同学;
- 前端能力一般,想用现成后台模板快速搭建界面的同学;
- 时间紧张,希望在可控范围内完成毕设,留出时间准备论文和答辩的同学。
如果你已经在准备Java面试里的一些框架原理,用这个项目来串联Spring Boot、MyBatis Plus、Spring Security或拦截器这些知识点,也是比较顺手的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈取舍:从SSM到Spring Boot的选型逻辑
技术选型这块我的建议非常直接:如果没有特殊要求,后端用Spring Boot + MyBatis Plus,前端用现成的Layui或较新一点的Vue 2后台模板,数据库用MySQL。这个组合不是因为它多前卫,而是它在“开发效率”和“毕业答辩”之间做了最好的折中。
2.1 为什么不再推荐JSP + Servlet组合
有些学校教材还在教JSP + Servlet,老师也许说不限制技术,但我的个人建议是:能不用就尽量别用。JSP的页面和后端耦合得太紧,前端改一个样式都需要重新部署应用,项目结构一乱,后期调试非常痛苦。而且Java Web领域现在的实际行情已经是前后端分离或半分离的天下,坚持用老旧方案,答辩时被问“你这个项目怎么部署”“前后端怎么联调”都会比较被动。
2.2 后端框架选择的具体理由
Spring Boot内嵌Tomcat容器,打包成jar就能直接跑,这就省掉了配置外部Tomcat的一大堆环境问题。MyBatis Plus能够把单表CRUD做成开箱即用的BaseMapper方法,对报名表、课程表这种基础数据的维护效率非常高;遇到复杂查询比如多表关联查询、分页条件查询时,又可以使用注解SQL或XML文件写自定义SQL,灵活性也高。
我在这次项目里直接在pom.xml中声明了依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2.3 前端方案怎么取舍
前端我没有从头写。并不是前端不重要,而是毕设时间就这么多,把时间花在后台管理系统的样式打磨上性价比太低。我使用了基于Layui的轻量后台模板,表格、表单、弹窗、分页这些组件都现成,配合后端返回的JSON数据直接渲染。如果你想稍微“现代感”一点,也可以用Vue 2 + Element UI做一个简单的前后端分离版本,但工作量会大不少,需要自己在本地处理跨域和打包部署。
2.4 环境版本建议
实践中最省心的组合是:JDK 1.8(除非老师要求更高版本,否则不要主动升级)、Spring Boot 2.7.x、MySQL 5.7或8.0、Maven 3.8。Spring Boot 3.x默认要求JDK 17,很多毕设环境装的是JDK 8,版本不匹配会引发编译错误,没必要给自己加风险。
3. 表结构设计:把学生、课程、选课、成绩的实体关系理顺
数据表设计是整个系统最基础也最重要的部分。表设计好了,后面的业务代码自然水到渠成;表设计乱了,后面每写一个功能都要在SQL里拼命救火。教务系统的核心表少说也有七八张,我按业务模块拆开讲一遍,没有提到的辅助表可以通过MySQL脚本自己补充。
3.1 用户、学生、教师的基础表
教务系统里要登录的人分三类,但账号体系可以是统一的。我采用了两层结构:一张sys_user保存登录账号和角色,一张student和一张teacher保存各自的业务扩展信息。用学号或工号关联,避免把学生特有的班级、院系等字段硬塞进用户表。
sql复制CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
role TINYINT NOT NULL COMMENT '1-管理员 2-教师 3-学生',
status TINYINT DEFAULT 1,
create_time DATETIME,
update_time DATETIME
);
CREATE TABLE student (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
student_no VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(50) NOT NULL,
gender TINYINT,
dept_id BIGINT,
major_name VARCHAR(100),
class_name VARCHAR(100),
enroll_year VARCHAR(10),
phone VARCHAR(20)
);
CREATE TABLE teacher (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
teacher_no VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(50) NOT NULL,
title VARCHAR(50),
dept_id BIGINT,
phone VARCHAR(20)
);
3.2 院系、专业与教室
sys_dept管理院系和专业,字段可以包含父级ID,形成树形结构。教室表classroom需要保存教学楼、门牌号、容量,后面排课和选课容量校验都会用到。
3.3 核心业务:课程与教学班
“课程”和“教学班”要分开设计。课程是稳定的静态信息(课程编号、名称、学分、总学时),而教学班是某个学期实际开设的一个班次,需要关联授课教师、上课时间、教室、容量。
sql复制CREATE TABLE course (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
course_no VARCHAR(20) NOT NULL UNIQUE,
course_name VARCHAR(100) NOT NULL,
credit DECIMAL(3,1),
hours INT,
course_type TINYINT COMMENT '1-必修 2-选修 3-公选'
);
CREATE TABLE course_schedule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
semester VARCHAR(20) NOT NULL COMMENT '如2024-2025-1',
course_id BIGINT NOT NULL,
teacher_id BIGINT NOT NULL,
classroom_id BIGINT,
week_day TINYINT COMMENT '1-7表示周一到周日',
start_section TINYINT COMMENT '开始节次',
total_section TINYINT COMMENT '持续节数',
total_capacity INT,
selected_count INT DEFAULT 0,
course_time_desc VARCHAR(200),
status TINYINT DEFAULT 0 COMMENT '0-待审核 1-已通过 2-已驳回'
);
3.4 选课表与成绩表
选课表是学生和教学班之间的关联,必须加上唯一约束防止同一学生重复选择同一个教学班。成绩表建议和选课表共用一份数据,因为一次选课最终对应一条成绩记录,分开两张表反而增加维护复杂度。
sql复制CREATE TABLE course_selection (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
schedule_id BIGINT NOT NULL,
student_id BIGINT NOT NULL,
select_time DATETIME,
UNIQUE KEY uk_schedule_student (schedule_id, student_id)
);
CREATE TABLE course_selection (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
schedule_id BIGINT NOT NULL,
student_id BIGINT NOT NULL,
regular_score DECIMAL(5,1) DEFAULT NULL,
exam_score DECIMAL(5,1) DEFAULT NULL,
total_score DECIMAL(5,1) DEFAULT NULL,
grade_point DECIMAL(4,2) DEFAULT NULL,
status TINYINT DEFAULT 0 COMMENT '0-已选课 1-成绩已录入 2-成绩已发布',
UNIQUE KEY uk_schedule_student (schedule_id, student_id)
);
这里有个容易犯的错:千万不要省掉唯一约束只靠代码判断,代码判断在高并发下可能同时通过,数据库唯一约束是最底层的防线。
4. 选课、成绩、教学班:三个核心模块的实现思路
接下来是实际编码环节。教务系统里最需要动脑子的三大块是选课业务、成绩业务、教学班管理,我逐个展开讲清楚实现逻辑。
4.1 选课业务:并发冲突才是这道题的关键考点
选课的基本流程是:学生浏览可选课程列表,选择一个教学班,系统判断没有重复选、时间没有冲突、容量没有满,然后插入选课记录,同时把该教学班的已选人数加一。
最容易被忽视的是“容量校验和人数更新”这两个动作必须保证原子性。我一开始也是先查该教学班的selected_count,如果小于total_capacity再执行更新,表面上没问题,但两个学生同一瞬间查询到的都是“还差一人”,然后同时插入选课记录,就会导致超选。
解决方案是在更新已选人数时加上条件:
java复制@Override
@Transactional(rollbackFor = Exception.class)
public Result<String> selectCourse(Integer scheduleId, Integer studentId) {
CourseSchedule schedule = scheduleMapper.selectById(scheduleId);
if (schedule == null) {
return Result.error("课程不存在");
}
if (schedule.getStatus() != 1) {
return Result.error("该课程未通过审核,不能选课");
}
// 检查是否已经选过
LambdaQueryWrapper<CourseSelection> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CourseSelection::getScheduleId, scheduleId)
.eq(CourseSelection::getStudentId, studentId);
if (selectionMapper.selectCount(wrapper) > 0) {
return Result.error("请勿重复选课");
}
// 检查上课时间冲突
List<CourseSelection> selectedList = selectionMapper.selectList(
new LambdaQueryWrapper<CourseSelection>().eq(CourseSelection::getStudentId, studentId));
List<Integer> scheduleIds = selectedList.stream()
.map(CourseSelection::getScheduleId)
.collect(Collectors.toList());
if (!scheduleIds.isEmpty()) {
List<CourseSchedule> chosenSchedules = scheduleMapper.selectBatchIds(scheduleIds);
for (CourseSchedule chosen : chosenSchedules) {
if (isTimeConflict(chosen, schedule)) {
return Result.error("上课时间与其他已选课程冲突");
}
}
}
// 原子更新:只有当前已选人数小于容量时才更新
int updated = scheduleMapper.increaseSelectedCountIfAvailable(scheduleId, schedule.getTotalCapacity());
if (updated == 0) {
return Result.error("课程容量已满");
}
CourseSelection selection = new CourseSelection();
selection.setScheduleId(scheduleId);
selection.setStudentId(studentId);
selection.setSelectTime(new Date());
selectionMapper.insert(selection);
return Result.success("选课成功");
}
对应的Mapper方法用Update语句实现原子判断:
java复制@Update("UPDATE course_schedule SET selected_count = selected_count + 1 " +
"WHERE id = #{scheduleId} AND selected_count < #{totalCapacity}")
int increaseSelectedCountIfAvailable(@Param("scheduleId") Integer scheduleId,
@Param("totalCapacity") Integer totalCapacity);
这就保证了“查容量”和“加数量”变成一个原子操作,无论多少学生同时抢同一门课,数据库行锁都会保证只有一个人能更新成功。
4.2 时间冲突判断:把周次和节次换算成可比较的维度
上课时间冲突的判断逻辑比较烦,因为课程表的course_time_desc字段很容易设计成“周一第3-4节”这样的中文描述,这对人友好,但对代码不友好。我的做法是在course_schedule表里拆出week_day、start_section、total_section三个数字字段,冲突判断就变成纯数学比较了。
java复制private boolean isTimeConflict(CourseSchedule a, CourseSchedule b) {
// 周几不同则不冲突
if (!a.getWeekDay().equals(b.getWeekDay())) {
return false;
}
int aStart = a.getStartSection();
int aEnd = a.getStartSection() + a.getTotalSection() - 1;
int bStart = b.getStartSection();
int bEnd = b.getStartSection() + b.getTotalSection() - 1;
// 只要两个区间有交集就认为冲突
return aStart <= bEnd && bStart <= aEnd;
}
这里有个小细节注意一下:如果排课表加入了“单双周”概念,字段里还需要加week_type,区分每周都上、单周上、双周上,否则判断逻辑会漏掉这种特殊情况。
4.3 成绩录入与绩点换算
教师端进入自己的课程列表后,可以对该教学班下的学生进行成绩录入。这里涉及一个业务权限校验:教师只能看到自己授课的教学班,不能看到其他老师的课,控制方式在查询时带上teacher_id条件即可。
成绩默认包含平时成绩和期末成绩,总评可以按比例折算,也可以由教师直接录入一个总评。绩点的计算规则在需求分析阶段就要和老师确认好,不同学校算法不一样。我采用了一个比较通用的折线算法:
java复制public Double calcGradePoint(Double score) {
if (score == null || score < 60) {
return 0.0;
}
if (score >= 95) {
return 4.5;
}
if (score >= 90) {
return 4.0;
}
if (score >= 85) {
return 3.5;
}
if (score >= 80) {
return 3.0;
}
if (score >= 75) {
return 2.5;
}
if (score >= 70) {
return 2.0;
}
if (score >= 65) {
return 1.5;
}
return 1.0;
}
在答辩时这部分比较容易被追问“绩点怎么算的”“成绩能不能导出Excel”,所以还需要提供一个导出功能,用EasyExcel或POI把成绩列表导出成Excel文件,这个点在我的实测中很容易引起老师好感,因为它明显超出了基础CRUD的范畴。
4.4 教学班管理:审核流是区分普通管理系统和教务系统的分水岭
教学班不能由教师随便发布,需要管理员审核。教师提交开课申请后记录状态为“待审核”,管理员在后台看到待审核列表,通过后学生才能看到这一教学班。这个审核状态机虽然简单,但论文的“业务流程设计”部分有东西可写,答辩时也能顺理成章地讲清楚“为什么不能让教师直接发布课程”。
5. 角色权限与登录态:教务系统的安全底线
教务系统的权限模型是答辩时几乎必问的内容,建议把权限设计作为独立章节写进论文里,同时在Demo演示时重点展示不同账户登录后看到的功能不同。
5.1 基于角色的访问控制模型
我这里使用简化的RBAC模型:用户表、角色、菜单/按钮权限。管理员、教师、学生都是角色,每个角色拥有一组可访问的接口或菜单项。实现方式也没有必要引入Spring Security那么重的安全框架——用Spring Boot拦截器配合自定义注解完全够用,而且代码是你自己写的,答辩时被问到底层原理也能答得出来。
实现方式是先定义一个@RequireRole注解:
java复制@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value();
}
再编写拦截器,在preHandle中从Session或Token中解出当前用户角色,判断是否匹配注解上的角色数组:
java复制public class RoleInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
if (requireRole == null || requireRole.value().length == 0) {
return true;
}
Object roleObj = request.getSession().getAttribute("role");
if (roleObj == null) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}");
return false;
}
Integer role = (Integer) roleObj;
List<String> allowedRoles = Arrays.asList(requireRole.value());
if (!allowedRoles.contains(String.valueOf(role))) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"msg\":\"无权限访问\"}");
return false;
}
return true;
}
}
然后注册拦截器并配置拦截路径:
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RoleInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/logout", "/captcha", "/error");
}
}
这样一个教师用户就算手动拼接/admin/student/list这样的URL,也会被拦截器挡在门外,不会因为“前端隐藏了按钮”就出现越权问题。
5.2 登录态存储:Session还是Token
系统是单体应用,不涉及多端分离部署,我选择了Session方案。登录成功后把userId、username、role、realName放入Session,比使用JWT更简单可靠,也天然支持服务端注销会话。如果做了前后端分离,那建议用JWT + Redis的替代方案,但坦白说毕设阶段SSO不是重点,先把Session方案跑通再说。
5.3 密码加密与验证码
密码直接存明文是绝对的硬伤,答辩时被老师扫一眼数据库就会扣分。我是用Spring Security里的BCryptPasswordEncoder对密码加密后再入库,登录时比对密文。不做登录验证码也可以,但系统里我建议加一个简单的算术验证码,体验会更好,也显得系统完整度高。
6. 调试中的真实坑点:并发选课、时间格式、逻辑删除
这章没有,我尽量写实。做这个项目时我遇到过一个非常经典的问题,排查了两天,最后发现是MyBatis Plus的配置问题,这类问题希望大家少走点弯路。
6.1 并发选课测试时数据不一致
第一个版本写完后我用POSTMAN并发请求选课接口,10个并发抢5个名额,结果成功了7个。排查过程如下:
- 先检查代码逻辑,确实写了
if (selectedCount < totalCapacity)再执行update; - 再检查数据库,发现
selected_count字段被更新了7次,说明多个请求都通过了一层校验; - 最后定位到更新语句是
UPDATE course_schedule SET selected_count = selected_count + 1 WHERE id = ?,但没有加selected_count < total_capacity条件,所以即使逻辑上“认为满了”,update还是能成功。
解决方法就是把容量判断合并进UPDATE语句的WHERE条件中,也就是我上面代码里写的increaseSelectedCountIfAvailable。这一步做完之后再次测试,5个名额就只会成功5个,多余请求提示“课程容量已满”。这个排查过程印象深刻,也建议你在论文里写进“系统测试与问题修复”章节。
6.2 LocalDateTime序列化问题
MySQL使用datetime类型,Java实体使用LocalDateTime类型来做时间字段,但接口返回JSON时时间字段输出格式是yyyy-MM-dd'T'HH:mm:ss,中间带个字母T,前端显示很难看。解决方式是给Jackson配置一个全局日期格式:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
不要每次在实体字段上加@JsonFormat注解去处理,容易漏掉某个字段。
6.3 逻辑删除字段导致查询失效
我用了MyBatis Plus的@TableLogic逻辑删除,配置好之后删除方法会自动变成UPDATE ... SET deleted = 1,而且所有查询都会自动追加deleted = 0。但我曾经把字段名写成了is_deleted,而实体字段是Integer deleted,没有在application.yml中配置全局逻辑删除字段名,导致查询SQL里出现了WHERE deleted=0但数据库里字段实际叫is_deleted,直接报错。
建议在application.yml里显式配置:
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
这样不管是字段名还是值都不用依赖默认值,排查问题时也会更清晰。
6.4 会话过期后Ajax请求变成重定向
登录状态失效后,访问任意受保护接口会被拦截器重定向到登录页。但管理员后台和教师端的前端页面很多地方是用Ajax异步请求后端JSON,如果是重定向,前端拿到的是302然后自动跳转登录页HTML,而不是JSON,这就导致页面弹不出登录提示,只会有加载失败。
解决办法是让拦截器识别请求类型:
java复制String requestedWith = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWith)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"登录已过期,请重新登录\"}");
} else {
response.sendRedirect("/login");
}
这样普通页面跳转登录页,Ajax请求则返回JSON码,前端接收后弹出提示并跳转登录页,体验会舒服很多。
7. 答辩环节:演示路径和高频问题怎么应对
很多同学项目做完了,但答辩时不知道怎么讲,三两分钟就把系统演示完了,剩下的时间被老师问得手足无措。这个系统在答辩前建议按下面的路径准备一遍演示脚本。
7.1 演示顺序建议
不要登录进去就随机点菜单,讲系统要有节奏:
- 先用管理员账号登录,展示系统首页的统计面板(学生数、教师数、课程数、本学期开课数)——这是整个系统给老师的第一印象;
- 展示管理员如何创建学生账号、录入教师信息、开设一门课程并审核教学班;
- 切换教师账号,展示教师能看到自己名下的授课列表,进入一门课后录入成绩;
- 切换学生账号,展示学生选课操作,最好现场演示“选课成功后再次点击选课提示不能重复选”的效果;
- 展示学生查看成绩和课表,如果做了成绩导出Excel,顺便导出一次。
流程走完大概五六分钟,时间刚刚好。
7.2 老师最容易追问的几个问题
-
问:选课时如何防止超选?
答:数据库唯一约束防止重复选课,数量校验通过UPDATE ... WHERE selected_count < total_capacity的原子更新完成(这是最有含金量的一问,别掉链子)。 -
问:密码是怎么存的?
答:BCrypt加密存储,不存明文。 -
问:如果并发量再大,这个系统哪里会先撑不住?怎么优化?
答:单机数据库行锁在高并发下会成为瓶颈,优化方向一个是选课队列化(先排队再异步处理),二是引入Redis缓存课程容量做预扣减,三是把选课服务拆成独立节点扩容。 -
问:为什么选MyBatis Plus而不是JPA?
答:开发效率高,复杂SQL可控性强,团队上手成本低。
7.3 论文写作时的一点点建议
论文的建议就是不要只写“实现”两个字,业务规则、流程图、E-R图、表结构说明都是大头。重点关注第三章“需求分析”和第四章“系统设计”,设计表的时候把字段注释都写上去,截图时表结构更清晰。测试部分尽量放真实测试数据,建议给三条角色各造两条演示数据,测试报告里的数据要能和演示时对得上。
我自己做完这个项目后一个很深的体会是:毕设项目的价值不在于功能数量多少,而在于你有没有把几件核心事情想透。选课并发、权限控制、数据关系设计这三块如果都能有理有据地讲出来,答辩现场基本就是稳定发挥。最后再分享一个小技巧:开发过程中务必用git init把每一版代码都管理起来,每次改完一个模块就提交一次,后面出了问题可以随时回滚,写论文时也能对应commit信息回忆起当时的修改意图,这一点在毕业季忙乱的日子里帮了我大忙。
