简介:这是一个基于Java的教务查询系统练手项目,使用SSM(Spring+SpringMVC+Mybatis)整合开发,并引入Shiro安全框架、C3P0数据源、log4j日志及Bootstrap前端框架,适合初学Java后端、希望熟悉SSM整合流程的开发者参考。压缩包共271个文件,大小35.32MB,包含55个Java源码、76个class编译文件、42个XML配置、28个JSP页面、31个jar依赖库,以及少量SQL脚本、样式表和文档,目录结构清晰,便于按代码、配置与页面分类查阅。项目实现了课程、教师、学生、选课等典型教务模块,能够帮助读者理解SSM分层架构、权限控制与数据库交互的完整代码组织方式。目前已有411人学习浏览,适合用来对照练习、巩固框架整合与教务场景开发基础。
1. 基于Java开发的教务系统:不只是增删改查,权限模型和并发选课才是分水岭
做过几个学期的课设辅导和项目重构之后,我越来越确认一件事:基于Java开发的教务系统,是Java Web方向最值得新手完整走一遍的项目,没有之一。它没有电商那种高并发秒杀场景,也没有推荐系统那种算法复杂度,但正好覆盖了Java后端日常工作的核心拼图——Spring Boot的工程结构、MyBatis的数据访问、MVC的分层设计、权限控制、事务管理,以及最让人头疼的并发选课。
很多初学者拿到这个题目,第一反应是把学生表、课程表、成绩表建好,然后写几个CRUD接口,跑起来给老师演示一遍就交差。但实际的教务系统痛点恰恰不在这几张表上:一门课被几百个学生同时抢选,数据库怎么保证不超选?成绩被老师重复提交,怎么保证幂等?一个学生能看哪些成绩、改哪些课,权限边界画在哪里?这些才是把课设水平拉开到工程水平的分水岭。
这篇文章适合两类人:一类是Java基础已经学完、正在找课程设计或毕业设计落地项目的学生;另一类是想在面试前攒一个能讲清楚设计取舍的完整项目的初级开发。我会从技术选型讲到建表,从权限模型讲到并发细节,最后补充排错经验——全程不绕弯子,直接照着做就行。
2. 教务系统的技术选型:单体应用加经典Java全家桶,为什么默认不碰微服务
2.1 先定边界:一个教务系统本质上在管哪几件事
开始写代码之前,先搞清楚教务系统这个标题到底在指什么东西。真实环境里的教务系统非常庞杂,排课、考务、教材、毕业审核都能塞进来,但以Java课程设计和大多数中小学校的需求来看,核心域就四件事:基础信息管理(学生、教师、班级)、课程与开课计划、选课与成绩管理、用户认证与权限控制。把这四块做扎实,系统就能真正投入使用;反过来,如果不定义清楚边界,很容易在“实验报告管理”这类边缘功能上消耗大量时间。
明确了边界,技术选型就清晰了。这是一个典型的中后台管理系统,并发量按学校规模算,峰值也就是几百个学生同时选课。这个量级下,单体应用就是最合理的选择,Spring Boot加MyBatis加MySQL这套组合拳足够扛住,而且生态成熟,出了问题网上随便一搜就有答案。微服务、消息队列、分布式缓存这些东西在这个场景下不但没有收益,反而会把自己的项目复杂度推高好几倍。
有人会问,那面试时聊微服务怎么办?我的看法是:在一个单体应用里把事务边界、接口设计、权限模型讲清楚,比硬拆几个服务更有说服力。你完全可以在项目描述里写一句“按业务模块划分包结构,未来可按需拆分”,这比没有流量就上微服务的空谈务实得多。
2.2 版本选型:JDK到底用8还是17,MyBatis还是MyBatis-Plus
Java环境变量的配置是很多新手的第一道坎,但只要你把JDK装好、JAVA_HOME配好、path里加上bin目录,然后命令行执行java -version能通过,这个前置就算完成了。真正需要纠结的是版本:JDK 8还是17?我调试过不少学生的项目,翻车案例大多是这两种:一是JDK 17编译却把Language level设成了8,报“源发行版 17 需要目标发行版 17”的警告;二是用JDK 8跑Spring Boot 3.x的项目,直接启动失败。Spring Boot 3.x要求JDK 17起步,而很多教材和网上的老教程还在用Spring Boot 2.x配JDK 8。
我的实用建议是:默认选JDK 8加Spring Boot 2.7.x,这是目前兼容性最稳的组合,所有第三方依赖都找得到对应版本,遇到报错也好排查。如果你是做毕业设计想体现新意,选JDK 17加Spring Boot 3.x也行,但尽量用MyBatis-Plus 3.5.3以上的版本,否则有兼容性问题。持久层框架我直接推荐MyBatis-Plus,别犹豫——它在MyBatis基础上帮你把单表CRUD的样板代码全部省掉了,你只需要写业务代码,而那些多表关联查询,用注解SQL或者XML都能解决。
前端这块,分两种情况:如果这是课程设计、时间紧,直接用JSP加Bootstrap,或者Thymeleaf加Semantic UI,所有页面渲染都在服务端完成,部署时一个WAR包打进Tomcat就行;如果是毕业设计或想积累求职作品集,用Vue 3加Element Plus做前后端分离,后端只出JSON接口。注意一点:前后端分离的项目在答辩时要演示跨域配置和联调过程,这本身就是加分项。
2.3 项目骨架:Maven多模块结构怎么搭才能让代码不烂在后面
很多人拿到Spring Boot,习惯把所有代码堆在一个工程里,controller、service、mapper、entity全都按技术分层。小项目没问题,但教务系统这种功能模块异常清晰的项目,更好的做法是按业务模块分包,而不是按技术角色分包。我自己的习惯是这样一个包结构:
edu-admin ├── pom.xml ├── edu-common # 通用模块:统一返回结果、异常处理、工具类 ├── edu-system # 系统模块:用户、角色、权限、登录 ├── edu-teaching # 教学模块:课程、开课、选课、成绩 └── edu-framework # 框架配置:Spring Security、MyBatis、Redis配置具体到每个模块内部,再用controller/service/mapper/entity分层。这种“先业务后技术”的组织方式,能让你在后期加功能的时候只动一个模块,而不是在一堆包名相似的文件里来回跳转。Maven的parent聚合工程负责统一管理依赖版本,子模块之间通过<dependency>互相引用。
在写第一行业务代码之前,我强烈建议你先把统一返回结果和统一异常处理建好。教务系统要跟前端或JSP页面打交道,如果每个接口返回的数据格式都不一致,联调时会让人崩溃。用一个Result<T>类包住code、message和data,再用@RestControllerAdvice捕获全局异常,把参数校验失败、业务异常、系统异常区分开处理,这个地基打好了,后面的开发速度会快很多。
3. 数据库设计是教务系统的地基:从E-R图到建表语句,一次把表结构定到可上线
3.1 核心表拆解:学生、课程、选课、成绩之间的关系不能只靠外键
数据库设计是整个教务系统里最不该省时间的一块,因为业务表关系一旦定错,后面返工的代价极大。教科书式做法是先画E-R图,但实际项目的图往往比教科书里复杂得多——因为真实的约束都在细节里。
教务系统的核心表大概有这几张:学生表、教师表、课程表、开课表、选课表、成绩表、用户表、角色表。先说课程与开课的区别:课程表存的是课程基本信息,比如课程代码、课程名称、学分、学时;而开课表记录的是“这个学期谁来上这门课”,包含授课教师ID、上课时间、上课地点、选课容量、已选人数。为什么必须拆开?因为同一门高等数学,春季和秋季很可能是不同老师上,时间地点也变了。很多新手只建一张课程表,结果一到排课就傻眼了。
再来看选课表和成绩表。选课表在关系上应该关联学生ID和开课ID,而不是直接关联课程ID——这样解锁学期这个维度,学生选的实际上是“这个学期的这门课”。成绩表则需要关联选课ID,因为一次选课记录对应一条最终成绩,分数录入、修改都在这张表上做。有些设计把成绩字段直接嵌到选课表里,这样虽然少一张表,但成绩的修改记录、补考记录就没地方记了,所以我宁可多拆一张表出来。
外键要不要建?我的建议是:开发阶段不建物理外键,但在业务逻辑里确保引用完整性。理由有二:一是物理外键在后期做数据迁移和数据清洗的时候是很大的负担;二是学校的选课系统经常要做删除和恢复操作,外键约束很容易让删除顺序变成噩梦。你可以通过索引和业务校验来维护一致性,“逻辑外键”是很多生产系统的实际做法。
3.2 建表语句实践:字符集、引擎、索引该怎么定
下面是学生表、课程表、开课表、选课表、成绩表的核心建表SQL。我用的是MySQL 8.0的语法,字符集用utf8mb4而不是utf8——因为utf8在MySQL里存不了emoji和部分生僻字,学生名字里有生僻字时你会感谢这个选择的。
-- 学生表 CREATE TABLE `student` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `student_no` VARCHAR(20) NOT NULL COMMENT '学号', `name` VARCHAR(50) NOT NULL COMMENT '姓名', `gender` TINYINT DEFAULT '1' COMMENT '性别:1-男 2-女', `class_name` VARCHAR(50) DEFAULT NULL COMMENT '班级', `major` VARCHAR(50) DEFAULT NULL COMMENT '专业', `status` TINYINT DEFAULT '1' COMMENT '状态:1-在读 0-离校', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_no` (`student_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; -- 开课表 CREATE TABLE `course_open` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `course_id` BIGINT NOT NULL COMMENT '课程ID', `teacher_id` BIGINT NOT NULL COMMENT '教师ID', `semester` VARCHAR(20) NOT NULL COMMENT '学期,如2025-2026-1', `capacity` INT NOT NULL DEFAULT '60' COMMENT '选课容量', `selected_count` INT NOT NULL DEFAULT '0' COMMENT '已选人数', `class_time` VARCHAR(100) DEFAULT NULL COMMENT '上课时间,如周一3-4节', `classroom` VARCHAR(50) DEFAULT NULL COMMENT '上课地点', PRIMARY KEY (`id`), KEY `idx_semester_course` (`semester`, `course_id`), KEY `idx_teacher_semester` (`teacher_id`, `semester`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开课表';selected_count这个字段是不是冗余?是的,但它是刻意为之的冗余,目的是让选课页展示剩余名额时不用每次都COUNT(*)去扫选课表。代价是每次选课和退课都要更新这个数字,事务里要保证更新顺序一致,否则会出现显示有坑位但实际选不进去的情况。索引方面,联合索引(semester, course_id)是查询最高频的路径,教师查询开课列表用(teacher_id, semester),这两组索引基本覆盖了99%的查询场景。
3.3 选课与成绩表:重复选课、重复录成绩怎么在数据库层防住
接下来是选课表和成绩表,这两张表的业务约束比前面几张都要特殊。
-- 选课表 CREATE TABLE `course_selection` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `student_id` BIGINT NOT NULL COMMENT '学生ID', `open_id` BIGINT NOT NULL COMMENT '开课ID', `select_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', `status` TINYINT DEFAULT '1' COMMENT '状态:1-已选 0-退课', PRIMARY KEY (`id`), UNIQUE KEY `uk_student_open` (`student_id`, `open_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课表'; -- 成绩表 CREATE TABLE `course_score` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `selection_id` BIGINT NOT NULL COMMENT '选课ID', `regular_score` DECIMAL(5,2) DEFAULT NULL COMMENT '平时成绩', `exam_score` DECIMAL(5,2) DEFAULT NULL COMMENT '考试成绩', `total_score` DECIMAL(5,2) DEFAULT NULL COMMENT '总评成绩', `score_level` VARCHAR(10) DEFAULT NULL COMMENT '等级:优秀/良好/中等/及格/不及格', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_selection` (`selection_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表';这里有两个容易忽视的设计决策。第一个是选课表的唯一键(student_id, open_id),这是防止重复选课的数据库兜底方案——哪怕你的接口被前端重复调用、或者消息重试,数据库这层都能拦下来。第二个是成绩表用selection_id做唯一键,意思是“一次选课只能有一条成绩记录”,老师重复提交成绩时走的是更新而不是新增,这保证了成绩的幂等性。有些设计会漏掉这两个唯一键,结果选课表里出现同一个学生选了同一门课两条记录,成绩表里出现同一学生同一门课多个分数——到那种时候修复数据的成本会让你怀疑人生。
4. 核心功能实现:登录认证、权限控制、选课并发,逐个写成可运行的代码
4.1 登录认证与权限模型:基于角色的访问控制(RBAC)怎么落地
教务系统至少有三类角色:学生、教师、管理员。它们对同一份数据的操作权限完全不同——学生只能查自己的成绩、选自己的课;教师能管自己课程的教学班、录成绩;管理员拥有全部权限。这种场景标准的解法是RBAC(基于角色的访问控制),在用户和角色之间建立多对多关系,角色再绑定权限。
Spring Security是Java生态里做认证授权最成熟的框架,但它也出了名地难学——尤其是它那一整套过滤器链,新手经常被绕晕。我的建议是:项目里用一个轻量级的实现,核心原理是登录成功后签发一个Token,后续请求通过Token识别身份,通过角色判断是否有权限。先把这个机制跑通,再去啃Spring Security的配置就轻松多了。下面是一个用拦截器做登录校验和角色鉴权的核心骨架:
@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 String uri = request.getRequestURI(); if (uri.startsWith("/api/auth/")) { return true; } // 从Header获取用户身份标识 String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } // 从Redis校验Token并取出用户基本信息 String userInfo = redisTemplate.opsForValue().get("login_token:" + token); if (userInfo == null) { response.setStatus(401); return false; } // 解析出用户ID和角色ID存入request,后续Controller直接用 request.setAttribute("currentUser", userInfo); // 管理端接口,校验管理员角色 if (uri.startsWith("/api/admin/") && !userInfo.contains("\"role\":\"admin\"")) { response.setStatus(403); return false; } return true; } }这段代码的逻辑很直接:拦截器里先放行登录接口,然后检查Token是否在Redis中存在,最后对管理端接口做一次简易的角色校验。用Redis存Token的好处是:当你把用户踢下线时,只需要删掉Redis里的键,不用等Token自然过期——这在学生毕业离校、教师离职的场景下太实用了。注意,真实项目里我不会在拦截器里用contains去判断角色,而是把用户信息解析成对象后按角色ID判断,这里是为了让你看清楚主流程。
配置上,在WebMvcConfigurer里注册这个拦截器,并指定拦截路径。用户表、角色表、用户角色关联表三张表加上加密存储的密码字段(BCrypt加密,绝对不要明文存密码),这个权限模型的基础就完整了。
4.2 选课接口的并发控制:超选问题不是玄学,是锁和事务的排列组合
把选课次数最多的接口拿出来单独聊——学生选课。如果你直接写一个“先查已选人数,若小于容量则插入选课记录、已选人数加一”的朴素实现,那么只要两个学生同时点选课,就可能出现超选——两人同时查到已选人数是59,容量是60,各自都认为可以选,结果是61人。这种情况在课设演示时很难复现,但在真实上线后的第一时间必然出现。
解决这个问题,最常用的方案是使用数据库的行锁,也就是SELECT ... FOR UPDATE,把“查询名额”和“更新名额”锁在同一事务里,保证同一门课在同一时刻只有一个事务在修改已选人数。下面这个方法是经过验证可行的:
@Transactional(rollbackFor = Exception.class) public Result<Void> selectCourse(Long studentId, Long openId) { // 1. 锁住开课记录,防止并发超选 CourseOpen open = courseOpenMapper.selectByOpenIdForUpdate(openId); if (open == null) { return Result.error("开课记录不存在"); } // 2. 检查名额 if (open.getSelectedCount() >= open.getCapacity()) { return Result.error("选课已满,无法选择"); } // 3. 防止重复选课(数据库唯一键兜底) int exist = courseSelectionMapper.checkExist(studentId, openId); if (exist > 0) { return Result.error("请勿重复选课"); } // 4. 插入选课记录 CourseSelection selection = new CourseSelection(); selection.setStudentId(studentId); selection.setOpenId(openId); courseSelectionMapper.insert(selection); // 5. 已选人数+1 courseOpenMapper.increaseSelectedCount(openId); return Result.success("选课成功"); }对应的MyBatis-Plus查询语句是selectByOpenIdForUpdate,在XML里写SELECT * FROM course_open WHERE id = #{openId} FOR UPDATE。这个FOR UPDATE就是在告诉数据库:把这行记录锁住,其他事务想改必须先等我提交。注意,锁要加在事务的最前面,让锁持有的时间最短;事务里尽量只做数据库操作,不要在锁范围内调用外部HTTP接口,更不要在锁范围内发短信、发邮件。
如果你的选课场景再大一些,可以用Redis的分布式锁,但不要过度设计。对于单个MySQL实例的教务系统,FOR UPDATE已经够了。这个方案的重心是:先把正确性保证住,再考虑性能优化。
4.3 成绩录入:事务边界和权限校验一个都不能少
成绩录入是教师端的核心操作。这个功能看起来简单——一个老师对着一张名单填分数然后保存,但有两个坑:一是老师只能改自己授课班级的成绩,二是成绩重复提交时要走更新而不是插入。
权限校验应该放在Service层,而不是等到Controller再去判断。因为Service层是业务逻辑的正确归属位置,Controller只负责参数接收和结果返回。老师登录后,我们根据Token里的教师ID去查开课表,确认这门课确实是他的,然后再执行后续的更新操作。成绩的计算逻辑,比如平时成绩占40%、期末成绩占60%,也要放在Service层,这样无论从哪个入口提交成绩,规则都一致。
这里有一个值得注意的细节:成绩表用selection_id做唯一键,意味着同一名学生的一门课只有一条成绩记录。教师第一次录入时走insert,再次提交时走update。判断是该insert还是该update,最稳妥的方式是先按selection_id查一次,存在就更新,不存在就插入。还有一种做法是使用MySQL的INSERT ... ON DUPLICATE KEY UPDATE语法,一条SQL搞定,但可读性稍差,我用的是前者——因为辅导的学生往往更容易理解和维护这种朴素的写法。
5. 避坑指南:教务系统开发中的六个高频翻车现场,从环境到数据一个不落
5.1 中文乱码:Tomcat、JSP页面、MySQL三层都要统一字符集
每一次我帮学生排查中文乱码,最后的结论几乎都是同一个:三层字符集没有统一。JSP页面显示的从上到下分别是请求编码、响应编码、数据库连接编码、数据库表字符集,任何一层不对,中文就会变成问号。
最典型的场景是:前端提交的中文数据存进数据库后变成“???”,或者页面显示乱码。解决方法是按下述清单检查:JSP或HTML页面头部设置charset=UTF-8;Spring Boot的application.yml里配置server.tomcat.uri-encoding=UTF-8;MySQL连接串中加上characterEncoding=utf8;数据库表和字段的字符集使用utf8mb4。这个方法的价值在于:趁项目刚开始做就统一字符集配置,能省掉后面大把排错时间。
如果你用的是IDEA开发,还有一个容易忽略的点:IDEA的全局文件编码、项目文件编码要和Properties文件的编码都设为UTF-8。多数情况下Properties配置文件默认是ISO-8859-1,你写中文注释进去基本必乱码。
5.2 数据库连接失败:Public Key Retrieval is not allowed和时区配置
这个报错在大学机房和Windows本地开发时极常见:MySQL 8.0连接时报Public Key Retrieval is not allowed。原因是MySQL 8默认使用caching_sha2_password认证插件,如果连接串没有指定allowPublicKeyRetrieval=true,驱动在无法安全获取公钥时会直接拒绝连接。
解决方法是修改JDBC连接串,加两个参数:
jdbc:mysql://localhost:3306/edu_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=trueserverTimezone必须显式设置,这也是一个高频报错点——不设置的话会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。设置成Asia/Shanghai就解决了。另外,MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver,和旧版的com.mysql.jdbc.Driver不一样,如果你的Spring Boot版本和驱动版本不匹配,启动时还会在这上面卡一会儿。
5.3 选课超卖:MyBatis二级缓存和FOR UPDATE打架
有一个坑非常隐蔽:你在Mapper上开启了MyBatis的二级缓存,同时又在选课查询上使用了FOR UPDATE。二级缓存会把查询结果缓存起来,但只要第一次查询时开课人数是59,缓存里就会一直记着59,后面的请求走缓存不查库,FOR UPDATE根本不会生效,超选问题就“死灰复燃”了。
解决办法很简单:禁用Mapper上的二级缓存,至少对选课这条链路不要使用。选课、库存这类写多读少的数据,追求缓存反而是给自己找麻烦。MyBatis的一级缓存是会话级别的,事务结束后自动失效,问题不大;二级缓存是跨会话的,默认没开也不会自动开,但在网上抄配置时容易不小心加进去。
5.4 Spring Boot启动失败排查:端口被占用和数据库连不上是最常见的两个原因
启动报错很多人第一反应是代码写错了,但我见过最多的情况其实是端口被占用。8080端口被别的进程占着,Spring Boot会提示Port 8080 was already in use,这时候在命令行执行netstat -ano | findstr 8080找到占用进程,然后杀掉或者改项目端口就行。
另一个容易忽视的启动问题:数据库没启动就去跑Spring Boot项目。如果你用的是本地MySQL,先确认MySQL服务是启动状态,Windows下可以在服务管理器里查MySQL服务,或者在命令行执行mysql -u root -p验证能否连上。数据库连不上时启动报错会持续很长一段时间,看起来像项目卡死了,实际上是一直在尝试重连。
5.5 前端传来的参数校验别全在Controller里写,Bean Validation更省事
很多新手习惯在Controller里写一大堆if (xxx == null) { return error; },一个方法里能堆十几行校验代码。教务系统里学生选课传的studentId、openId,成绩录入传的成绩分数,都需要校验,如果全用if堆,代码会越来越难维护。
我一般用@Validated加JSR-303注解来做参数校验:在实体类的字段上加@NotNull、@Min、@Max注解,Controller方法参数上加@Validated,框架会自动完成校验,不通过时抛异常由全局异常处理器转成统一返回结果。这样做的好处显而易见:代码量少一半,校验规则跟着字段走,不会因为Controller里漏写校验导致脏数据进库。
5.6 数据库密码直接写死在配置文件里
最后这条也许不算“技术错误”,但在求职简历上聊到项目时它是加分点。教务系统的application.yml里,数据库账号密码、Redis密码直接明文写在配置里,这在本地开发没问题,但代码一旦推到GitHub上被公开,整个数据库就裸奔了。解决方式有两种:如果项目部署在Linux服务器,用环境变量注入密码;配置中心(比如Nacos)对这个量级的项目来说又太重了。环境变量方案最合适,在application.yml里写成${DB_PASSWORD:默认值},既能本地跑通,部署时又不会泄露真实密码。
6. 把教务系统从课设推到可部署:从打包到部署的四个实用细节
课程设计答辩通过不等于项目结束,把Java编写的教务系统真正部署到服务器上跑起来,才算走完了一个完整闭环。这个阶段的技术含量不亚于写业务代码——很多同学代码在本地能跑,部署到Linux服务器上问题一个接一个。
我的习惯是用Maven打成可执行的JAR包部署,而不是打WAR包放Tomcat。Spring Boot内置了Tomcat,java -jar一条命令就能启动,省去了单独管理外部Tomcat的麻烦。打包命令是:
mvn clean package -DskipTests打包成功后,在target目录下会生成一个edu-admin.jar。用scp传到服务器上,然后执行:
nohup java -jar edu-admin.jar --spring.profiles.active=prod > app.log 2>&1 &--spring.profiles.active=prod指定生产环境的配置,nohup让程序在后台运行,输出日志写到app.log。首次部署容易踩的坑是服务器MySQL的字符集没配置,导致写入中文出现乱码;还有防火墙没放开8080端口,外部访问不到。服务器上执行mysql -u root -p进入MySQL后,运行show variables like 'character%';,确认character_set_server是utf8mb4,不是的话修改my.cnf后重启服务。
部署完之后,验证环节很多人都做得不够。我的做法是至少在服务器上把这三条链路完整走一遍:学生登录选课,教师登录录入成绩,管理员查看统计报表。特别要关注的是选课高峰期时的表现——找三五个同学同时点选同一门只剩一个名额的课,看最终选课人数是否超过容量。别把这个验证当成走过场,它直接检验第4.2节里FOR UPDATE是否真的生效了。
还有两个细节值得做:一是定期备份数据库,对于教务系统来说数据就是命,我的做法是写一个简单的定时脚本,每天凌晨用mysqldump全量备份并用find -mtime +30自动删除30天前的备份文件;二是给管理端的敏感操作加上日志记录,谁在什么时间改了成绩、调整了选课容量,都要能追溯,这在课程答辩和真实使用中都是加分项。
说实话,我自己第一次部署教务系统时,光是因为服务器时区没同步导致时间错乱,就排查了一整晚。后来养成的习惯是:新环境第一步先检查和配置字符集、时区、防火墙,全部确认无误再部署业务代码。希望这些经验能让你少走几段弯路,把部署这件事一次做顺利。
本文还有配套的精品资源,点击获取