每到毕设开题那阵子,总有人过来问我:Java方向的题目到底选什么好?既要能短期写完,又要在答辩的时候讲得出东西。问得多了,我发现题库里那些"校园管理系统"被翻来覆去地选,有人嫌它普通、觉得没亮点,可实际能把一套SSM版本的校园综合管理平台从需求到表结构、从前端到后端完整做下来,并且把每一个"为什么"都想明白的人,真不多。
这套系统我前后搭过三遍,从最初的课程设计到后来帮别人做的完整毕业设计项目,踩过的坑、重构过的表、答辩被追问过的问题,全都攒在这里了。这篇文章不是让你背代码,而是把做这套项目的完整思路拆给你看:需求怎么拆、模块怎么划、表怎么设计、SSM整合哪些地方容易翻车、答辩前哪些问题一定要提前准备好。如果你正准备做Java + SSM方向的毕设,或者想拿一套"校园全流程管理"的项目练手,这一篇应该能让你少走不少弯路。
1. 选题定调:一套SSM校园管理系统覆盖了哪些真实场景
1.1 系统全景:校园管理服务平台的核心模块
很多同学拿到题目之后的第一反应是打开文档写代码,结果写了两周发现逻辑一团乱。做这类系统,第一步必须先想明白一件事:"全流程"到底全在哪里。
校园管理场景里,日常能落地的业务大致有这几条线:
| 业务线 | 学生端 | 教师端 | 管理端 |
|---|---|---|---|
| 选课管理 | 查看开课列表、选课、退选 | 查看自己课程的学生名单 | 维护开课计划、设定容量 |
| 成绩管理 | 查看个人成绩 | 录入成绩、导出成绩单 | 审核成绩发布 |
| 教室/场地预约 | 预约空闲教室或活动室 | 预约教学场地 | 审批预约申请 |
| 信息发布 | 查看公告通知 | 发布课程通知 | 全局公告管理 |
| 基础数据维护 | 维护个人信息 | 维护个人资料 | 管理学生、教师账号 |
把这张表列出来,系统的边界就清楚了。不是所有校园功能都要做进去,比如学籍档案、教务排课、薪资管理这些是另一个量级的东西,毕设周期内贪多嚼不烂,反而会让SQL和页面失控。我建议的核心模块是:用户管理、课程与选课、成绩管理、公告通知、场地预约,外加一个简单的数据统计面板。这五六个模块已经足够撑起"综合管理平台"的定位,也足够把SSM的三层架构、事务控制、拦截器、分页这些知识点全部体现出来。
1.2 为什么选SSM而不是Spring Boot
这个问题几乎是我每次帮别人规划毕设时都会被问到的。我的回答分两面看。
如果你已经熟练掌握了Spring Boot的自动配置、起步依赖,那确实没必要退回去用SSM。但对大多数毕设选手来说,SSM有三个价值是Spring Boot替代不了的:
第一,SSM的配置是显式的。Spring的IOC容器、SpringMVC的DispatcherServlet、MyBatis的SqlSessionFactory,全都要自己手动装配一遍,这个过程会把框架原理逼着你搞懂一遍。答辩的时候老师一问"SpringMVC的执行流程是什么",你没有亲手配过,答起来就是背课文;你亲手配过,回答的时候脑子里是有画面的。
第二,SSM在技术面试里依然是高频考点。虽然现在企业普遍用Spring Boot,但面试八股问来问去还是Spring IOC、AOP、MVC执行流程、MyBatis动态SQL这些底层东西。毕设做SSM,相当于把这些知识点串了一遍,简历上也能拿得出手。
第三,环境兼容性更稳。老一点的实验室机器、教程资料、参考项目大多基于SSM + JSP/JQuery这代技术栈,遇到问题随手百度能搜到大量对应场景的答案。Spring Boot新版本的写法迭代很快,版本不匹配时查资料反而是个坑。
当然,完全不用Spring Boot也不现实。我实际做的时候会在项目中保留一部分Spring Boot风格的约定(比如统一返回结果、全局异常处理),让代码习惯向现代Java开发靠拢,这个过渡思路值得推荐。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模块拆解:三种角色怎么切分权限边界
2.1 角色权限:三套菜单、一套拦截规则
校园管理系统最忌讳的事情,是学生页面里能看到"教师管理"的按钮,点进去还能把老师账号删了。这不光是页面显示的问题,而是后端接口没有做权限校验。我在设计时就按"菜单可见性 + 接口拦截"两层来做。
用户表里用一个role字段区分三类角色:1表示超级管理员,2表示教师,3表示学生。登录成功后,把用户对象放到Session里,前端根据角色动态渲染菜单。比如学生端只有"首页、课程列表、我的选课、成绩查询、场地预约、公告",教师端多出"我的开课、成绩录入、预约审批",管理端则是完整的系统管理。
后端拦截的逻辑要更严格。我写了两个拦截器:
- LoginInterceptor:检查Session里有没有登录用户,没有就重定向到登录页。
- RoleInterceptor:根据请求URL前缀判断角色。比如 /admin/** 要求role=1,/teacher/** 要求role=2,/student/** 要求role=3,不匹配就直接返回到无权限提示页。
有一个容易忽略的细节是:前端的角色菜单是给用户看的,后端的URL权限才是真正防越权的。你永远不能假设用户只点页面上的按钮,直接用Postman或者浏览器地址栏构造请求是很容易的事,所以接口层面一定要拦。
2.2 核心业务流程:从选课到成绩发布的全链路
拿"选课"这条主流程来说,完整的业务链路是这样的:
教学秘书(管理员)先在后端创建学期、导入课程 → 教师确认自己的授课信息 → 学生登录系统,看到可选课程列表,点击选课 → 系统校验课程是否可选、容量是否已满、是否重复选择 → 选课成功后课程已选人数+1 → 学期结束教师录入成绩 → 管理员发布成绩 → 学生查询成绩并看到绩点。
这条链路里有两个点特别值得写进论文和答辩稿里:
一是选课时的并发控制。如果有100个学生同时抢一门只剩5个名额的课,用单纯的"先查再插"就一定会超选。我后面在第四章会讲具体的处理思路,这里先记住结论:要么用数据库的SELECT ... FOR UPDATE加悲观锁,要么用UPDATE的条件判断做乐观锁,二者选一实现。
二是状态流转。课程有"可选/选课中/已截止/已结课",预约有"待审批/已通过/已驳回",账号有"正常/禁用"。状态字段用TINYINT存数值,比直接用字符串更省空间、查询更快,也方便扩展。但代码里一定要写完整的常量定义,不然时间一长自己都忘了2代表什么。
3. 数据库设计:从用户表到业务表的关联与落地
3.1 用户体系:单表还是多表
这是很多毕设里最容易犹豫的地方。我的建议是:主账号表 + 扩展信息表。
用户表只存登录相关的核心字段:
sql复制CREATE TABLE sys_user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号',
password VARCHAR(100) NOT NULL COMMENT '密码,BCrypt加密存储',
salt VARCHAR(32) DEFAULT '' COMMENT '盐值,若用加盐MD5才需要',
real_name VARCHAR(50) NOT NULL COMMENT '真实姓名',
role TINYINT NOT NULL DEFAULT 3 COMMENT '角色:1管理员 2教师 3学生',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1正常 0禁用',
email VARCHAR(100) DEFAULT '' COMMENT '邮箱',
phone VARCHAR(20) DEFAULT '' COMMENT '手机号',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_role (role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
学生和教师如果有个性化字段(学号、班级、工号、职称),再建student_info和teacher_info表,用user_id做外键关联。这样设计的好处是:登录验证永远只查核心表,速度快;扩展字段不臃肿;权限校验的逻辑也简单。
密码安全这里多说一句,不要明文存密码,这不仅是毕设规范问题,更是企业级的底线。推荐两种方案:要么用Spring Security自带的BCryptPasswordEncoder,要么用MD5(密码 + 随机盐) 存两次。前者更现代,后者更简单也提得出"加盐防彩虹表"的答辩点。
3.2 业务表:课程、选课、成绩、预约
课程表是选课模块的根基,设计时要把"容量"和"已选数量"这种冗余字段放进去,方便列表页直接展示和判断:
sql复制CREATE TABLE course (
id INT PRIMARY KEY AUTO_INCREMENT,
course_no VARCHAR(20) NOT NULL UNIQUE COMMENT '课程编号',
course_name VARCHAR(100) NOT NULL COMMENT '课程名称',
teacher_id INT NOT NULL COMMENT '授课教师ID,关联sys_user.id',
credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT '学分',
capacity INT NOT NULL DEFAULT 60 COMMENT '选课容量',
selected_count INT NOT NULL DEFAULT 0 COMMENT '当前已选人数',
classroom VARCHAR(50) COMMENT '上课教室',
week_day TINYINT COMMENT '星期几,1-7',
section VARCHAR(20) COMMENT '节次,如第3-4节',
semester VARCHAR(20) NOT NULL COMMENT '开课学期,如2024-2025-1',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1可选 0不可选',
KEY idx_teacher (teacher_id),
KEY idx_semester (semester)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';
选课表要加联合唯一约束,这是兜底防止重复选课的关键:
sql复制CREATE TABLE course_selection (
id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT NOT NULL COMMENT '学生ID',
course_id INT NOT NULL COMMENT '课程ID',
select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1选课中 2已退选 3已结课',
UNIQUE KEY uk_student_course (student_id, course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课记录表';
成绩表建议采用"学生 + 课程 + 成绩"的格式,同时做联合唯一约束:
sql复制CREATE TABLE score (
id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT NOT NULL,
course_id INT NOT NULL,
score DECIMAL(5,2) COMMENT '百分制成绩',
remark VARCHAR(255) COMMENT '备注',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_student_course_score (student_id, course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表';
场地预约表要预留审批状态:
sql复制CREATE TABLE room_reservation (
id INT PRIMARY KEY AUTO_INCREMENT,
room_name VARCHAR(50) NOT NULL COMMENT '场地名称',
reserver_id INT NOT NULL COMMENT '预约人ID',
purpose VARCHAR(200) COMMENT '预约用途',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME NOT NULL COMMENT '结束时间',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待审批 1已通过 2已驳回',
approve_remark VARCHAR(255) COMMENT '审批意见',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_reserver (reserver_id),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场地预约表';
3.3 表设计里的几个细节规范
这一节是纯经验,踩了坑才懂:
- 所有表都用InnoDB引擎,外键约束在真实开发里经常会造成死锁和迁移麻烦,所以表之间只用逻辑外键,不建物理外键。论文里写"通过业务逻辑保证数据一致性",比"物理外键一堆"反而更像生产实践。
- 字符集统一用utf8mb4,不要用utf8。utf8在MySQL里是utf8mb3,存不了emoji和生僻字,出问题的时候表情包会直接变成问号。
- 时间字段尽量用DATETIME,不用TIMESTAMP。TIMESTAMP有2038年问题,还有时区转换的坑。
- 每个表都保留create_time和update_time,后面写统计面板、排查问题都靠它们。
- 布尔状态用TINYINT(1)表示"是/否",不要用BIT;角色这种多值状态用TINYINT加常量定义,不要用SET。
4. SSM整合实现:配置文件、拦截器与分页查询的关键细节
4.1 配置文件分工:三层容器到底谁管谁
SSM的配置确实比Spring Boot啰嗦,但你只要搞清楚一个核心问题就不会乱:Spring容器负责Service和Mapper,SpringMVC子容器负责Controller,MyBatis通过SqlSessionFactoryBean交给Spring管理。
web.xml里要做两件事:配置ContextLoaderListener加载Spring根容器(applicationContext.xml),配置DispatcherServlet加载SpringMVC容器(spring-mvc.xml)。核心配置片段是:
xml复制<!-- 1. Spring根容器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 2. SpringMVC前端控制器 -->
<servlet>
<servlet-name>springMvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 3. 字符编码过滤器,必须放在所有过滤器最前面 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
spring-mvc.xml里最容易被忽略的是静态资源放行。如果你把DispatcherServlet映射到"/",所有请求都会被它接管,CSS、JS、图片全部404。必须要加:
xml复制<mvc:annotation-driven />
<context:component-scan base-package="com.campus.controller" />
<!-- 静态资源放行 -->
<mvc:resources mapping="/static/**" location="/static/" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
applicationContext.xml里核心是数据源、SqlSessionFactory、Mapper扫描和事务:
xml复制<context:component-scan base-package="com.campus.service" />
<context:property-placeholder location="classpath:jdbc.properties" />
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml" />
<property name="mapperLocations" value="classpath:mapper/*.xml" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.campus.mapper" />
</bean>
<!-- 开启注解事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
记住一个原则:Controller扫描只放在spring-mvc.xml里,Service/Mapper扫描只放在applicationContext.xml里。两边都扫会导致事务失效或者Bean重复创建,这个坑我见得太多了。
4.2 登录拦截与权限校验:拦截器如何配合Session工作
登录拦截器是实现权限控制的骨架。核心思路是:在preHandle方法里从Session中取用户,没有就跳登录页;取到了再根据当前请求URL判断角色权限。
java复制public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
SysUser user = (SysUser) session.getAttribute("loginUser");
if (user == null) {
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
String uri = request.getRequestURI();
if (uri.startsWith(request.getContextPath() + "/admin/") && user.getRole() != 1) {
response.sendRedirect(request.getContextPath() + "/403");
return false;
}
if (uri.startsWith(request.getContextPath() + "/teacher/") && user.getRole() != 2 && user.getRole() != 1) {
response.sendRedirect(request.getContextPath() + "/403");
return false;
}
return true;
}
}
这个拦截器要注册进spring-mvc.xml:
xml复制<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<mvc:exclude-mapping path="/login" />
<mvc:exclude-mapping path="/static/**" />
<bean class="com.campus.interceptor.AuthInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
注册的时候记得把登录接口和静态资源路径排除掉,否则连登录页的CSS都会因为没登录而跳转,直接死循环。
4.3 分页查询与统一返回结果:让后端少写一百行重复代码
列表页做分页是必然需求。手写limit和count其实不难,但每个Mapper都要维护两个SQL,工作量翻倍。我推荐用PageHelper,一行代码搞定分页:
java复制@Override
public PageInfo<Course> queryCoursePage(CourseQuery query, int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Course> list = courseMapper.selectByCondition(query);
return new PageInfo<>(list);
}
有一个很重要的前置条件:PageHelper的startPage必须紧跟在查询语句之前才能生效,中间不能隔任何数据库操作。另外在mybatis-config.xml里要确认分页插件是在settings之后、mappers之前。
统一返回结果是我建议所有接口都遵循的规范。前端不管是JSP还是Ajax,拿到一个固定格式的JSON总比"有时返回字符串、有时返回对象"好处理得多:
java复制public class Result<T> {
private Integer code; // 1成功 0失败
private String msg; // 提示信息
private T data; // 数据
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 1;
r.msg = "操作成功";
r.data = data;
return r;
}
public static <T> Result<T> error(String msg) {
Result<T> r = new Result<>();
r.code = 0;
r.msg = msg;
return r;
}
}
配合全局异常处理器,把业务异常和系统异常统一兜住,Controller里就不用到处try-catch,代码会清爽很多。
5. 实操中高频踩坑:从绑定异常到静态资源404的排查记录
5.1 报错一:Invalid bound statement (not found)
这是SSM项目里出现频率最高的报错,没有之一。现象是调用Mapper接口方法时,MyBatis提示找不到对应的SQL语句。
排查链路我建议按三步走:
- 打开Mapper接口,确认方法名和XML中的id完全一致,大小写也要一致。
- 确认XML的namespace是Mapper接口的全限定名,不是简单类名。这个写错的概率极高。
- 确认编译后的target目录里XML文件存在。如果只有class没有xml,在pom.xml里加一段resources配置把xml一起打包。
xml复制<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
用IDEA的同学尤其注意第三种情况,有时候代码看着没问题,就是target里没有xml,clean之后再重新编译就好了。
5.2 报错二:CSS样式全部丢失,页面像裸奔
页面能打开但没有任何样式,十有八九是DispatcherServlet的URL映射把静态资源拦了。前面已经提到,在spring-mvc.xml里加<mvc:resources mapping="/static/**" location="/static/" />就能解决。
还有一个藏得很深的情况:如果你用了Shiro或者Spring Security这类安全框架(我不建议毕设项目上这两个庞然大物,但有人就是想试),拦截器对静态资源的放行配置也要一起改。单纯配了SpringMVC放行,安全框架又把CSS拦了,一样是裸奔。
5.3 报错三:PageHelper分页不生效,查出来还是全量数据
这个问题典型的症状是:PageInfo对象里total是全部记录数,但list也是全部记录,分页等于白做。
原因通常是PageHelper的版本和MyBatis版本不兼容。PageHelper 5.x配合MyBatis 3.4.x是稳妥组合,如果用了PageHelper 6.x或者更老的1.x版本,可能出现startPage被忽略的情况。另一个原因是startPage之后执行了查询,但这条SQL被嵌套在另一个方法内部,查询时机分离导致ThreadLocal里的分页参数被清掉了。最稳妥的解决方式就是前面说的:startPage和查询语句之间不要插任何其他操作。
5.4 乱码问题:数据库乱、页面乱、响应体乱
乱码通常有三个来源,一次排查以下三个地方基本能解决:
| 症状 | 位置 | 修改方式 |
|---|---|---|
| 页面中文乱码 | JSP页面头部 | 加 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> |
| 数据库中文乱码 | JDBC连接串 | url后面加?characterEncoding=utf8&useSSL=false |
| 请求参数乱码 | web.xml | CharacterEncodingFilter,encoding设UTF-8,forceEncoding设true |
另外,数据库连接串里如果用了utf8而表结构是utf8mb4,部分字符可能写入失败或显示异常。保持DB字符集、连接串、页面编码三处都是UTF-8,是最省心的方案。
5.5 一个容易被忽略的问题:日期时间格式
前台表单提交过来的"2024-12-20 14:30"字符串,后台直接用Date接收会报400。解决方案是给实体类的日期字段加@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,或者在spring-mvc.xml里配置一个全局的日期转换器。我建议两个都配上,因为查询参数和请求体里的日期格式经常不一致。
6. 答辩高频追问与我的几点开发体会
6.1 把这些追问准备好,答辩不会慌
做完系统只是第一步,答辩时要能讲清楚"为什么这么设计"。我把高频问题整理成一个清单:
- 为什么用SSM模板而不直接用Spring Boot?——回答思路:SSM配置显式,能体现对框架原理的掌握;分层清晰;MyBatis半自动化SQL,灵活性和可控性强。
- SpringMVC的执行流程是什么?——回答思路:请求先到DispatcherServlet,通过HandlerMapping找到Controller方法,经过适配器调用,返回ModelAndView,经视图解析器解析,响应客户端。
- 事务注解为什么加在Service层?——Controller太薄,管的是参数和响应;DAO层是单条SQL操作,事务粒度太细;Service层承载业务逻辑,一个方法包含多次数据库操作,事务必须在这里统一控制。
- 并发选课怎么保证不超选?——两种方案都答:悲观锁用SELECT ... FOR UPDATE先锁行,判断容量再更新;乐观锁用UPDATE course SET selected_count = selected_count + 1 WHERE id = ? AND selected_count < capacity,受影响行数为0则说明已满。
- 密码做没做加密?——一定要说做了BCrypt加盐,或者MD5加盐,防止数据库泄露后密码被反查。
6.2 几条掏心窝的开发建议
做这类管理系统,我最大的感受是:不要一头扎进代码里,先把表和流程画明白。我在纸上画了整整三天的表结构和状态流转,真正写代码只用了两周。表设计一旦定了,后面所有业务都是在上面填充逻辑,改表比改代码痛苦一百倍。
开发顺序上,先做登录和用户管理,这是所有模块的入口;再做课程列表和选课,这是系统的核心业务;然后做成绩,最后做公告和场地预约。核心链路跑通之后,其他模块就是在复制粘贴这个套路。
遇到报错的时候,先看控制台日志,再去找答案,不要直接把报错贴到搜索引擎。大多数问题日志里已经写得很明白了,比如ClassNotFoundException会告诉你是哪个类没找到,NullPointerException会告诉你是哪一行。排查问题这个过程,本身就是答辩时的素材,你亲手解决的坑越多,讲起来就越有说服力。
这套系统做完,你可以继续在它上面做扩展:引入Redis做验证码和会话共享,引入RabbitMQ做选课异步削峰,引入Vue3把前端重写成前后端分离。项目天花板足够高,就看你想走多远了。
