
简介一套面向JavaWeb初学者的教务管理系统项目基于J2EE技术体系涵盖登录、找回密码、修改密码、注销等基础流程并按学生、教师、教务员、系统管理员四类角色划分功能。学生端支持成绩查询、选修与考级报名、学籍信息维护及考级成绩查询教师端可查询与修改个人信息教务员端提供成绩管理、报名学生名单查看等操作管理员则负责用户管理。资源包共223个文件压缩后2.6MB以82个Java源码及对应class文件、38个JSP页面、6个jar依赖和1个SQL脚本为主另含Eclipse工程配置、README说明等导入Tomcat 7与JDK1.8环境即可运行。项目采用MVC、DAO、Servlet、JSP、Filter技术分层结构清晰适合学习者对照源码理解业务流程与经典设计模式。目前已有11657人学习下载系统还预留部分待完善功能便于二次开发或作为课程设计参考。1. 教务管理系统javaweb项目它到底是什么样的存在如果你在搜索引擎里敲下「教务管理系统javaweb项目」大概率是带着一个具体任务来的要么是毕业设计需要交差要么是刚学完 Java Web 想找一个完整项目练手。这个标题背后对应的是最经典的 Java Web 教学项目类型——一套供学校或培训机构管理学生、课程、成绩、教师和选课信息的后台系统。它通常需要覆盖三个核心角色管理员、教师、学生、三类核心业务维护基础数据、安排教学任务、记录和查询成绩而且绝大多数版本都跑在 Tomcat 上前端由 JSP 渲染数据落在 MySQL 里。这些年我帮不少人排过这类项目的坑也动手做过两个相似结构的系统一个给某高校实训用一个用来当内部教程的载体。这类项目能长盛不衰不是因为它技术新恰恰相反它把 Servlet、JSP、JDBC、Session、请求转发与重定向这些 Java Web 的老底子全部串了起来数据库设计上又碰得到多对多关系、级联删除、权限控制这些真实业务问题。对新人来说它是能把书本缝合起来的那根线对想快速交付毕业设计的人来说它又是一个足够立体、演示效果好的 Demo——真把它做透了你简历上写「独立开发教务管理系统」一点不虚。不过有个反直觉的事实要先说在前面这类项目最大的技术难点不是代码写不出来而是「不知道把代码放在哪儿、建哪些表、角色之间怎么联动」。下面我会把项目拆分到能直接复现的程度包括技术选型怎么做、数据库表怎么设计、核心功能怎么实现、部署时有哪些坑以及最后怎么让它看起来像一个有质量的完整项目。2. 技术选型与项目结构先想清楚这套系统该用哪根骨架2.1 为什么「老一套」反而最稳JSP Servlet MySQL 的取舍逻辑选技术栈之前要认清这类项目的现实处境它是教学项目、毕设项目不是高并发工业项目。所以选型的第一原则不是「新」或「酷」而是「你能否讲清楚每一层在干什么」。最常见的方案是 JSP Servlet MySQL Tomcat纯手写 JDBC 或用一个小工具类封装数据库操作。这个组合的优点很具体Servlet 负责接收请求和控制页面跳转JSP 负责展示数据职责边界清楚答辩时能把 MVC 结构说得明明白白。没有引入 Spring 全家桶业务逻辑完全裸露出问题容易追查适合学习。部署简单打一个 war 包丢进 Tomcat 的 webapps 目录就能跑不依赖配置中心和云端基础设施。但也有同学一开始就上了 Spring Boot MyBatis Plus。我不反对甚至如果你已经能熟练用 Spring Boot 做一个带登录注册的小系统那用 Spring Boot 会更省事。可如果你的真实水平是刚弄懂 Servlet 的生命周期硬上 Spring Boot 只会把自己卡在注解、依赖注入和自动装配的围墙外出了问题连排查方向都没有。有一个折中方案值得知道用 Servlet JSP 做主体结构但把数据库操作封装成 DAO 模式。也就是说控制器层用 Servlet持久层自己写一个 BaseDao 封装通用增删改查业务层可以简单放在 Service 类里。这类项目里这个结构的性价比是最高的——代码量不会太失控答辩时也能顺畅解释后续想改成 SSM 或 Spring Boot 也有了清晰的迁移路径。我实际做过的模拟项目X用的就是这套结构Tomcat 8.5 JDK 8 MySQL 5.7。不追求新版本的原因只有一个网上能找到的排错经验大部分都基于这个组合遇到问题搜起来快得多。2.2 包结构怎么摆从 demo 到什么「可扩展」的目录布局包结构决定了一件事你的代码是看起来像作业还是看起来像工程。很多新手会把所有类随手丢进默认包这个习惯在 200 行代码内没问题但撑到一千行以上就会变成黑匣子。一个常规且合理的包结构如下com.example.education ├── controller前端控制器层放 Servlet │ ├── LoginServlet.java │ ├── StudentServlet.java │ ├── CourseServlet.java │ └── ScoreServlet.java ├── service业务逻辑层 │ ├── StudentService.java │ ├── CourseService.java │ └── ScoreService.java ├── dao数据访问层 │ ├── StudentDao.java │ ├── CourseDao.java │ └── ScoreDao.java ├── entity实体类也可叫 model 或 pojo │ ├── Student.java │ ├── Teacher.java │ ├── Course.java │ ├── Score.java │ └── User.java ├── util工具类 │ ├── DBUtil.java │ └── StringUtil.java └── filter放拦截器/过滤器 └── LoginFilter.javaWeb 目录结构则遵循标准 Java Web 布局src/main/java所有 Java 代码src/main/webapp/WEB-INF/web.xml核心配置文件Servlet 映射、过滤器、欢迎页src/main/webapp/JSP 页面按功能分目录存放例如 admin/、teacher/、student/有一个细节值得强调JSP 页面尽量不要直接放在 webapp 根目录下而是放在 WEB-INF 内层或者按角色分子目录。放在根目录任何人都能通过 URL 直接访问绕过登录限制。用 LoginFilter 拦截请求的同时也要把 JSP 物理隔离这才是双保险。很多早期版本的教务管理系统翻车就翻在这里。2.3 目录结构里的「分层思想」怎么在答辩里讲清楚如果你需要拿这个项目去答辩或展示光是目录分层还不够你得能解释为什么这么分。这套结构对应的是最经典的「三层架构」界面层ViewJSP 页面只负责用 HTML JSTL/EL 把数据渲染出来业务层Service处理业务规则比如选课不能超过 6 门、成绩只能由授课教师录入数据层DAO负责与 MySQL 交互执行 SQL一层只做一件事下层不该知道上层的存在。举个例子ScoreServlet 接收页面传来的学号和课程号它不该自己拼 SQL而是把学生信息对象传给 ScoreService让 Service 判断该学生是否选了这门课然后再调用 ScoreDao 执行插入操作。这种「Servlet 薄、Service 居中、DAO 厚」的做法就是为了让代码可维护、可替换。将来你从 Tomcat 部署迁移到 Spring BootService 和 DAO 的代码几乎可以原样带走换掉的只是 Controller 层。2.4 依赖和文件清单一个可运行的骨架需要哪些部分用 Servlet JSP 手写项目的另一个好处是不需要 Maven 也能跑。你可以用传统方式建一个 Dynamic Web Project然后引入必要的库文件/库作用是否必需javax.servlet-api或 jakarta.servlet-api编译 Servlet 用运行时由 Tomcat 提供必需mysql-connector-javaJDBC 连接 MySQL必需jstl-1.2.jarJSP 页面中使用 c:forEach 等标签推荐JSP 标准标签库JSTL对应依赖配合 EL 表达式渲染集合数据推荐数据库这边你需要准备一份建表脚本通常在项目里命名为init.sql或school.sql内容包括用户表、学生表、教师表、课程表、选课表、成绩表以及几个管理员/教师账号的初始数据。到这一步你的项目骨架已经搭好了。下一章进入数据库设计——这是多数教务管理系统跑不起来或者逻辑混乱的根源。3. 数据库设计五张核心表如何编排才能不打架3.1 从业务需求倒推表结构用户、学生、教师、课程、成绩的关系不要一上来就画 E-R 图正确顺序是先把业务场景写清楚。一个最基本的教务管理系统至少要回答这几个问题谁能登录系统答案是三种身份共用一个登录入口所以需要一个用户表来统一账号密码和角色类型学生和课程是什么关系一个学生选多门课一门课也能被多个学生选这是典型的多对多关系需要中间表选课记录表教师和课程是什么关系通常一门课只有一个授课教师一个教师可以带多门课是一对多成绩挂在哪里成绩应该挂在「某学生、某课程的某次选课记录」上把这些问题写成表结构就清晰了。我建议表结构至少包含以下五张核心表这也是这类项目的标准配置。3.2 核心建表 SQL参考一份可以直接用起来的结构下面这份 SQL 是按 MySQL 5.7 / 8.0 的语法写的字段取名遵循常见习惯主键、外键关系明确你可以直接拿来当项目基础-- 用户表统一登录用 CREATE TABLE sys_user ( id INT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 密码可用MD5加密, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, role TINYINT NOT NULL COMMENT 角色1管理员 2教师 3学生, status TINYINT DEFAULT 1 COMMENT 状态1启用 0禁用, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; -- 学生信息表 CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT NULL COMMENT 性别M/F, major VARCHAR(100) DEFAULT NULL COMMENT 专业, class_name VARCHAR(100) DEFAULT NULL COMMENT 班级, phone VARCHAR(20) DEFAULT NULL, user_id INT DEFAULT NULL COMMENT 关联sys_user.id, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; -- 教师信息表 CREATE TABLE teacher ( id INT NOT NULL AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL COMMENT 教师工号, name VARCHAR(50) NOT NULL, title VARCHAR(50) DEFAULT NULL COMMENT 职称, department VARCHAR(100) DEFAULT NULL COMMENT 所在院系, user_id INT DEFAULT NULL COMMENT 关联sys_user.id, PRIMARY KEY (id), UNIQUE KEY uk_teacher_no (teacher_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT教师信息表; -- 课程表 CREATE TABLE course ( id INT NOT NULL AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL COMMENT 课程编号, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 0 COMMENT 学分, teacher_id INT DEFAULT NULL COMMENT 授课教师ID关联teacher.id, max_students INT DEFAULT 60 COMMENT 选课人数上限, semester VARCHAR(20) DEFAULT NULL COMMENT 开课学期如2025-2026-1, PRIMARY KEY (id), KEY idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 选课记录表学生与课程的多对多中间表 CREATE TABLE student_course ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL COMMENT 关联student.id, course_id INT NOT NULL COMMENT 关联course.id, selected_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id,course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课记录表; -- 成绩表 CREATE TABLE score ( id INT NOT NULL AUTO_INCREMENT, student_course_id INT NOT NULL COMMENT 关联student_course.id, score DECIMAL(5,2) DEFAULT NULL COMMENT 成绩分数, grade_point DECIMAL(3,1) DEFAULT NULL COMMENT 绩点, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, PRIMARY KEY (id), UNIQUE KEY uk_sc (student_course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;这份 SQL 里有两个关键设计值得解释第一表名和字段名都用小写加下划线这是 MySQL 的惯例也避免在 Linux 部署时因大小写敏感产生「表不存在」的错觉。第二成绩表挂的是student_course_id而不是直接挂student_id course_id。这个设计的好处是成绩只可能出现在「确实选过课」的记录上不会出现某个学生没选课却有成绩的脏数据。查询成绩时走一条关联路径score → student_course → student/course逻辑是闭合的。3.3 外键到底建不建我的建议与理由以我经手过的多个类似项目来看教务管理系统一般不建物理外键而是建立索引。比如在student_course的 student_id 和 course_id 上建普通索引在course表的 teacher_id 上建索引。原因有两个物理外键会拖慢删除和更新操作。如果你想删除一门课外键约束会强制你先处理掉所有选课记录业务逻辑反而更绕。逻辑外键让代码更灵活。你在 Service 层做是否允许删除的判断比数据库抛外键约束异常更可控报错信息也友好。但有一个地方要保留唯一索引就是student_course表上的(student_id, course_id)联合唯一索引。这个索引在数据库层面守住了「同一个人不能重复选同一门课」的底线就算并发请求同时插入数据库也会拒绝第二条。这类教务系统的数据量不大但唯一约束一定要靠数据库保证不能只写在 Service 里。3.4 初始化数据怎么写管理员账号与测试数据准备建完表后的第一件事不是写代码而是插入测试数据。至少要有一个管理员账号、一名教师、一名学生、两门课程和几条选课记录。常见做法是放在init.sql里INSERT INTO sys_user (username, password, real_name, role) VALUES (admin, 21232f297a57a5a743894a0e4a801fc3, 系统管理员, 1), (1001, e10adc3949ba59abbe56e057f20f883e, 张三老师, 2), (2023001, e10adc3949ba59abbe56e057f20f883e, 李四同学, 3);说明一下密码字段不能存明文上面放的是两个典型示例——21232f...是 admin 的 MD5e10adc...是 123456 的 MD5。实际开发里我建议至少用 SHA-256 加盐但考虑这是教学项目MD5 也勉强够你需要知道它的弱点就行。登录校验时把用户输入的密码做同样的摘要运算再比对就能避免密码在数据库中裸奔。测试数据不要造太复杂的有一个学生选了课、有一个学生没选课、有一门课程人数已满就够覆盖大部分功能了。数据越简单开工越早。4. 功能实现登录、权限拦截、选课、成绩录入怎么落地4.1 统一登录与 Session 管理用 Filter 守住第一道门登录功能是所有教务系统的基础但很多项目把登录写成一锤子买卖——校验完密码就完事了后续每个页面再做权限判断时又重复写一遍改起来痛不欲生。这显然教训深刻。正确做法是登录校验只做一次后续所有请求交给过滤器统一拦截。登录 Servlet 的核心逻辑如下写在一个LoginServlet.java里WebServlet(/login) public class LoginServlet extends HttpServlet { private SysUserDao userDao new SysUserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password MD5Util.encrypt(req.getParameter(password)); String role req.getParameter(role); // 页面传入1/2/3 SysUser user userDao.login(username, password, Integer.parseInt(role)); if (user ! null) { // 登录成功后把用户身份写进 Session HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); // 30分钟超时 resp.sendRedirect(req.getContextPath() /index.jsp); } else { req.setAttribute(errorMsg, 账号或密码错误或角色不匹配); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }逻辑说明登录时把角色也作为查询条件之一意思是「学生账号只能从学生入口登录」避免学生拿自己的账号去登录教师后台。这里用了MD5Util.encrypt()对密码做摘要后再比对所以SysUserDao.login()里对应的 SQL 就要写成SELECT * FROM sys_user WHERE username? AND password? AND role?参数说明session.setMaxInactiveInterval(30 * 60)设置会话 30 分钟无操作自动失效。这个值可以根据场景调整如果是要展示给老师用可以放宽到 60 分钟如果是演示环境30 分钟更安全。转身写过滤器逻辑很直接WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); // 放行登录相关资源、静态资源 if (uri.contains(login) || uri.endsWith(.js) || uri.endsWith(.css) || uri.endsWith(.jpg) || uri.endsWith(.png)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); Object loginUser session ! null ? session.getAttribute(loginUser) : null; if (loginUser null) { // 未登录则重定向回登录页 resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }注意这行req.getSession(false)—— 传false表示「如果当前没有 Session 就返回 null而不是新建一个」。这样可以避免一种安全漏洞未登录用户访问任意页面如果你不传参调用getSession()容器会为他创建一个新 Session白白浪费内存且给攻击者拿到 SessionID 的机会。4.2 学生选课如何避免超选、重复选、选满后继续选选课是教务系统里业务逻辑最重的功能值得用一个小节单独展开。需求拆解后有三条规则学生不能重复选择同一门课学生选课时该课程未达到人数上限一个学生每学期选课总门数不超过上限比如 6 门这三个逻辑都应该放在 Service 层做不在 Servlet 里拼代码。写一个CourseService方法签名设计为「检查 保存」两步public class CourseService { private CourseDao courseDao new CourseDao(); private StudentCourseDao scDao new StudentCourseDao(); public String selectCourse(int studentId, int courseId) { // 1. 查课程信息 Course course courseDao.findById(courseId); if (course null) { return 课程不存在; } // 2. 查该课程当前已选人数 int selectedCount scDao.countByCourseId(courseId); if (selectedCount course.getMaxStundents()) { return 该课程选课人数已满; } // 3. 查该学生是否已经选过 if (scDao.exists(studentId, courseId)) { return 您已选过该课程请勿重复选课; } // 4. 查本学期选课总门数 int totalSelected scDao.countByStudentId(studentId); if (totalSelected 6) { return 本学期选课已达上限6门; } // 5. 通过所有校验执行插入 scDao.insert(studentId, courseId); return 选课成功; } }这段代码里最容易踩坑的是第 2 步到第 5 步之间的并发窗口两个学生同时点击选课都查到已选 59 人于是都通过校验最后课程变成 61 人。解决思路有两条由数据库兜底student_course表有联合唯一索引重复选课被拒绝但「超过人数上限」并不会被数据库拒绝由数据库锁兜底用SELECT ... FOR UPDATE锁住课程记录后再判断人数代价是并发性能下降但在教务系统的并发量级下完全可接受我一般会选择「唯一索引 乐观锁」。在course表加一个版本号字段选课更新时检查版本号。不过大多数教学项目根本没这个并发压力把规则写在 Service 层里已经够用你只需知道这个边界在哪里就行。4.3 成绩录入与查询教师页面最实用的 SQL 形态成绩相关的页面通常需要显示一个二维表横轴是课程纵轴是学生。但在数据库里成绩存的是纵表结构每个学生一门课一条成绩记录。所以实现时免不了要写「根据教师 ID 查课程」再「根据课程 ID 查所有选课学生及成绩」。这里给出一个比较顺手的 SQL 写法SELECT s.student_no, s.name student_name, c.course_name, sc.id sc_id, sc2.score FROM student_course sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id LEFT JOIN score sc2 ON sc2.student_course_id sc.id WHERE c.teacher_id ?重点说明这段 SQL 的三个设计意图用JOIN而不是在 Java 代码里做多次查询一次拿全表格需要的数据用LEFT JOIN关联成绩表因为有的学生选了课但老师还没给分成绩记录不存在此时score字段为 NULL带了课程 id 或课程名作为后续条件教师页面上可以选择「本学期课程」然后只展示该课程下的选课名单成绩录入的页面一般就是一张表格每行末尾放一个输入框和一个更新按钮。提交时按student_course_id做 update。这里要强调一个细节更新成绩要记录操作时间方便追责和复核。可以在score表加一列update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP级联更新一劳永逸。4.4 教师模块和学生模块公共逻辑课程列表与选课状态如何展示学生登录后看到的课程列表不只是课程信息还要附加「当前登录学生与该课程的关系」——是已选、未选、还是人数已满。这个状态判断写在 SQL 里效率更高SELECT c.id, c.course_name, c.credit, t.name teacher_name, c.max_students, (SELECT COUNT(*) FROM student_course sc WHERE sc.course_id c.id) AS selected_count, EXISTS(SELECT 1 FROM student_course sc2 WHERE sc2.course_id c.id AND sc2.student_id ?) AS selected_flag FROM course c LEFT JOIN teacher t ON c.teacher_id t.id WHERE c.semester ?selected_flag用EXISTS子查询只返回 0 或 1JSP 页面里就能直接判断按钮状态已选置灰、未选显示「选课」按钮、人数已满显示「满员」。这个查询是学生模块使用频率最高的一条 SQL执行计划完全走索引性能没有问题。5. 部署运行从 IDEA 到 Tomcat 的全过程与三个必调参数5.1 用 IDEA 创建项目并关联 Tomcat二十分钟跑通的最小步骤如果你从未跑通过 Java Web 项目照着下面的典型步骤走一遍思路就顺了。这里假设你用的是 IntelliJ IDEA Tomcat 8.5 MySQL 5.7这也是一套经过验证的常见配置。第一步创建工程。IDEA 里 New Project → 左侧选 Jakarta EE或 Java Enterprise→ 勾选 Web ApplicationApplication Server 选择你本机的 Tomcat 路径。如果版本较新没有这个模板就创建普通 Java 项目然后手动添加 Web 模块结构。第二步配置依赖。传统 Web 项目不需要 Maven只要在/WEB-INF/lib下放 JDBC 驱动包mysql-connector-java-5.1.49.jar或 8.x 版本和 JSTL 的 jar 包即可。第三步配置数据源。写一个工具类核心只有 20 行public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/school_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }参数说明useUnicodetruecharacterEncodingutf8是中文数据不出乱码的前提useSSLfalse是跳过本地连接时的 SSL 告警serverTimezoneAsia/Shanghai解决 MySQL 8.x 与 JDBC 驱动的时间区报错。如果是 MySQL 5.7serverTimezone可以不要但留着也没错。第四步配置 web.xml。Servlet 3.0 以后可以完全用WebServlet注解代替web.xml 里只需要配置欢迎页和全局编码过滤器welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list5.2 部署到本地 Tomcatwar 包与 exploded 目录两种方式IDEA 里最省事的运行方式是点击 Run → Edit Configurations → 新增 Tomcat Server → Local → Deployment 页签 → 点加号 → 选 Artifact → 选择xxx:war exploded。注意这一步有个细节war 包和 war exploded 的区别是前者打包后解压运行后者直接以解压目录运行。从学习和调试的角度我建议用 war exploded。改 JSP 页面保存后 Tomcat 直接生效不用重新打包调试效率高很多。改 Java 代码则需要 Debug 模式热部署或重启容器但至少不用等打包。真正交付部署的时候才用 war 包。在 IDEA 里 Build → Build Artifacts → Rebuild会在out/artifacts/xxx_war/下产出xxx.war。把这个文件拷贝到 Tomcat 的webapps目录启动 Tomcat 后它会自动解压并运行。访问地址是http://localhost:8080/xxx/login.jsp其中/xxx是上下文路径通常与 war 包同名。5.3 部署期最容易翻车的三个地方与配置确认部署期我总结过三个高频翻车点写出来给你当排查清单第一个是端口冲突。Tomcat 默认 8080如果你机器上装了其他服务占用了这个端口Tomcat 启动过程会在 catalina.out 里报几十行异常很容易让人误判代码问题。排查方式启动前先用命令确认端口占用情况并换个端口或释放占用。第二个是 JDBC 驱动版本与 MySQL 版本不匹配。MySQL 5.7 用 8.x 的驱动也能跑但首次连接时会报 SSL 或时区错误反过来MySQL 8.0 用旧版驱动会直接报「Public Key Retrieval is not allowed」。解决方法是按数据库版本选择驱动5.7 用 5.1.498.0 用 8.0.30 以上并在 JDBC URL 上补allowPublicKeyRetrievaltrue。第三个是 JSP 页面中文乱码。这个跟 IDE 默认编码强相关。IDEA 默认文件编码要统一设置为 UTF-8JSP 顶部声明% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %另外请求参数要加req.setCharacterEncoding(UTF-8)否则表单提交的中文到后端会变问号。这三处齐了乱码问题基本绝迹。5.4 验收怎么看跑通一套最小业务闭环的标准部署成功后用一套固定的验收路径来确认系统是好的而不是「能打开就是成功」验收项操作预期结果登录用 admin 账号登录跳转到管理员首页无 500 错误学生管理新增一个学生再刷新列表新学生出现在列表中且无乱码课程开设用管理员添加一门课程并指定教师教师端能看到该课程学生选课用学生账号登录选择一门课程选课成功后按钮变灰已选记录出现在个人中心成绩录入用教师账号登录给选课学生录入成绩学生端能看到成绩会话控制不登录直接访问 index.jsp被重定向到登录页如果上面全绿说明这个系统是真的「通」了。很多宣称「已完成」的项目卡在第三或第四步就断了——课程加不进去、选课选不上这种半架子的比跑不起来更麻烦。6. 避坑指南教务管理系统开发里的五个高频问题与排查路径6.1 现象启动 Tomcat 找不到 ServletHTTP 404原因有两个方向一是WebServlet(/login)的映射路径写错比如路径多了后缀或少了斜杠Servlet 注册不匹配二是 IDEA 里 Artifact 没有把src/main/java下的 .class 文件带进去常见于新建 Web 项目时目录标记不完整IDEA 没把该目录识别为源码目录。解决检查Project Structure → Artifacts → Output Layout里有没有将编译输出放入/WEB-INF/classes。如果没有手动加上。Servlet 路径则确认你的请求 URL 与注解完全一致大小写敏感。6.2 现象登录成功后跳回登录页无限循环原因基本都在过滤器上LoginFilter对login.jsp做了拦截之前检查了 Session 里的登录标记但页面里存在某个请求比如图片、CSS还没从过滤器放行导致页面永远拉不到静态资源从而刷新跳转。解决过滤器中优先放行以/login开头的路径、以.js/.css/.png/.jpg结尾的资源。用一句话概括过滤器只拦「需要登录才能访问的业务页面」静态资源永远放行。6.3 现象数据库中中文正常页面显示乱码原因JSP 文件本身的物理编码和pageEncoding声明不一致。IDEA 默认可能创建的是 GBK 编码的 JSP但你在声明里写了 UTF-8浏览器按下 UTF-8 解析两种编码打架。解决不用猜直接看字节。用编辑器强制转换为 UTF-8 无 BOM 格式然后打开 JSP 第一行检查声明。另外IDEA Settings → Editor → File Encodings三处全部设成 UTF-8Global Encoding、Project Encoding、Default encoding for properties files一劳永逸。6.4 现象SQL 报错「Unknown column xxx in where clause」原因多半是物理外键没建却用了逻辑外键但 Java 实体类里的字段命名与数据库列名不一致。比如 Java 类里叫studentId数据库列叫student_idSQL 里直接写了studentIdMySQL 找不着。解决DAO 层写 SQL 时一律用数据库列名不要用 Java 字段名。像? as student_id这类别名问题只存在于查询结果映射里。这个坑每个人都会踩戴上数据库列名的眼镜写 SQL 就没事了。6.5 现象选课接口被反复调用出现重复选课数据原因前端按钮没有在提交后立即禁用用户快速点两下两个请求都到了后端或者浏览器刷新缓冲重新提交了上一次请求。解决MySQL 内的UNIQUE KEY uk_student_course (student_id, course_id)是最后的防线但它只能针对同一学生同一课程。如果问题扩散到其他表后端 Service 在提交前必须重新查询一次状态不能信任前端传来的「已检查」标志。前端按钮在 ajax 提交后置为 disabled属于常规操作也要做掉。7. 进阶验证与代码卫生让教务系统经得起答辩和复查项目跑通只是起点真正让这个教务管理系统区别于「下载的模板」的是下面几个进阶动作。每一步花费时间不长但对印象分的提升非常明显。7.1 增加一个数据校验层杜绝空值和非法输入在 Service 层的方法入口加一道参数校验。比如新增学生时学号为空、学号重复、姓名为空都要拦截用统一的ParamException抛出去Servlet 里 catch 后返回友好提示不要等 SQL 执行时报DataIntegrityViolationException。这样做有三个直观收益代码健壮性提升一个档次答辩时能讲出「系统性异常处理」为后续迁移到 Spring Boot 时使用Validated做铺垫。7.2 把 SQL 全部收拢到 DAO 层加一层日志开关最影响代码卫生的一件事任何代码里不允许散落拼接 SQL 字符串。把 SQL 集中放进 DAO 类并用一个简单的控制台开关打印 SQL 和参数。你可以建一个SqlLogUtil在 DAO 执行前后打印当前 SQL 和参数列表。排查问题的时候这层日志能省出一小时起步的时间。7.3 密码加盐升级至少换成 SHA-256如果答辩时有人问你「密码明文存储怎么办」你会措手不及。把 MD5 升级为 SHA-256 加随机盐操作是一小时内的改动注册时生成随机盐存入数据库登录时取出盐重新计算摘要比对。把实现写清楚这已经是生产级的密码存储思路了远超毕业设计的要求。7.4 从 JSP Servlet 到 Spring Boot 的迁移路线如果你想在简历上写「熟悉主流开发框架」但又从没迁移过传统 Java Web 项目这个项目是最好的练手素材。迁移路径明确实体类和 DAO 层基本上可以直接复用把 JDBC 手写改为 MyBatis 或 MyBatis PlusService 层逻辑整体保留只替换创建接口的写法Servlet 替换为 Spring MVC 的 ControllerWebServlet(/login)变成PostMapping(/login)JSP 替换为 Thymeleaf 或继续用模板引擎。这个迁移本身就说明你理解这两套技术栈之间的关系面试时有话可讲。7.5 验证功能的最后一件事做一次完整的「陌生环境部署」所谓陌生环境部署就是换一台电脑新装 JDK、MySQL、Tomcat不给任何备注只给代码和初始化脚本看能不能两步跑起来。这一检查的意义在于它暴露你在本地环境的隐藏依赖。常见翻车点是数据库密码写死在 JDBC URL 里、MySQL 版本影响建表语法、Tomcat 版本与 Servlet API 不匹配。做到这一步系统才算真正交付。做教务管理系统这类项目最大的教训从来不是代码写不出来而是「写完了不敢从零部署一遍」。我自己的习惯是——项目收尾前把初始化脚本从建库到建表整体跑一遍再把 war 包放到干净的 Tomcat 里启动一次所有验收项手动走一轮之后才敢说这项目是完成的。如果你正卡在某个具体环节别急着怀疑自己先把数据表和请求路径捋清楚——这套系统本质上就是「谁通过什么身份、对哪张表、做什么操作」三个问题的展开。希望能帮到你。本文还有配套的精品资源点击获取