简介:基于 Java+Mysql 实现的教室信息管理系统,面向高校学生在数据库课程设计、毕业设计或大作业阶段的实践需求,重点训练数据库设计基本方法与编程实现能力,帮助学习者完成从需求分析、流程图与功能模块图设计,到 E-R 图绘制及转换为满足 3NF 关系模式的完整过程。资源共 90 个文件,压缩包约 944KB,其中包含 25 个 Java 源文件、53 个编译后的 class 文件、1 个 SQL 脚本、5 个 XML 配置、2 个 docx 课程设计报告以及 README 说明,源码、数据库脚本与文档分层存放,便于按需查阅。包内附带的课程设计报告对选题背景、系统设计思路及数据库结构进行了系统说明,适合需要参考完整项目流程而非直接照搬代码的入门与进阶学习者。目前已有 124 人学习下载,可作为课程设计答辩准备、毕业设计初期立项或工程实训的参考资料。
1. 基于 Java+MySQL 的教室信息管理系统,为什么值得作为第一个完整项目做透
基于 Java+MySQL 的教室信息管理系统这个题目,在课程设计和毕业设计里出现频率非常高。它没有复杂算法,管理对象也很直白:教室编号、所在楼栋、楼层、座位数、有没有多媒体、当前是否被占用。但正是这种“简单”,让它成为检验 Java 基础和 MySQL 基本功最合适的试场。连接数据库、字符乱码、驱动加载、事务回滚,几乎每一个环节都会在开发中碰到一次,而这些恰恰是电梯里被问得最多的 Java 面试题相关知识点。
这套系统解决的实际问题很具体:教务人员能按楼栋、容量、设备情况筛选空闲教室,学生或教师可以在线提交预约申请,管理员审核后教室状态自动变为“使用中”,到期或取消后恢复“空闲”。整个流程覆盖了从列表展示、条件查询、新增修改,到一对多关联和事务操作的完整链路。适合的人群,是从“跟着视频敲”过渡到“独自搭一套”的学生,也适合准备投递后端岗位、想亲手写一遍 JDBC 的求职者。
做这个项目时不需要一上来就选重框架。把 Java、Servlet、JSP、JDBC、MySQL 这五件事逐个填实,每个模块都有可演示的操作路径,课程设计报告也能写出内容。接下来就看数据库怎么设计,以及 Java 代码怎么和 MySQL 高效协作。
2. 教室信息管理系统的数据库设计:ER 图、建表 SQL 与初始数据
答辩老师通常不逐行读代码,但一定会翻数据库设计部分。他们看的是:实体划分是否合理,外键怎么建,状态字段用什么类型,有没有为查询频繁的列加索引。这一章先把 ER 图画出来,再用 MySQL 8.0 把表落库,同时把初始化数据准备好。设计合理的话,后面的 DAO 和 Java 代码写起来会格外顺。
2.1 四个实体和两段关系的 ER 图设计
教室信息管理系统的核心实体可以拆成四张表:管理员、教室、预约记录、使用记录。管理员与预约之间是一对多,教室与预约之间也是一对多。下面这张表可以先作为报告里 ER 图的文字版:
| 实体 | 关键属性 | 作用 |
|---|---|---|
| admin | id, username, password, real_name | 系统登录账号,区分管理员与普通用户 |
| classroom | id, building, room_no, floor, capacity, media, status | 教室基础信息,status 表示空闲、使用中或维修 |
| reservation | id, classroom_id, use_date, time_slot, purpose, applicant, phone, status | 教室预约申请表,教室与申请人的关联体 |
| usage_log | id, reservation_id, start_time, end_time, operator | 实际使用记录,用于统计教室利用率 |
字段设计要遵循两个原则:第一,教室和预约尽量拆开,不要在教室表里塞一整条 JSON 预约数据,否则后续统计、冲突判断都很别扭;第二,状态字段用 TINYINT 存数字,不要用字符串存“空闲/使用中”,数据库层面用数字加 COMMENT 说明,Java 里再定义常量对照。这样在 MySQL 面试题常考的索引效率问题上也站得住脚。
2.2 直接可执行的 MySQL 8.0 建表 DDL 脚本
打开 MySQL 命令行或 MySQL Workbench,先建库再建表。数据库字符集不要用默认的 latin1,统一用utf8mb4,否则前端传入的中文名称写入后就是问号或乱码。
CREATE DATABASE IF NOT EXISTS cims DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE cims; CREATE TABLE classroom ( id INT PRIMARY KEY AUTO_INCREMENT, building VARCHAR(20) NOT NULL COMMENT '楼栋名,如博学楼', room_no VARCHAR(20) NOT NULL COMMENT '教室编号,如 A201', floor INT DEFAULT 1, capacity INT DEFAULT 60, media TINYINT DEFAULT 1 COMMENT '1=有多媒体 0=无', status TINYINT DEFAULT 0 COMMENT '0=空闲 1=使用中 2=维修', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_building_room (building, room_no), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教室基本信息表';这里有几个值得写进设计报告的点。UNIQUE KEY uq_building_room联合唯一约束保证同一栋楼里不会插入两个 A201;KEY idx_status让“按状态筛选教室”的查询走索引;create_time用 DATETIME 而不是 TIMESTAMP,避免 2038 年问题和时区换算困扰。数据库选择 InnoDB 也很关键,事务和行级锁在后面对接 Java 预约代码时是必需品。
接着建预约表,教室与预约通过外键关联:
CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, classroom_id INT NOT NULL, use_date DATE NOT NULL, time_slot VARCHAR(20) NOT NULL COMMENT '时间段,如 08:00-10:00', purpose VARCHAR(200) COMMENT '使用用途', applicant VARCHAR(50) NOT NULL COMMENT '申请人姓名', phone VARCHAR(20), status TINYINT DEFAULT 0 COMMENT '0=待审核 1=已通过 2=已拒绝 3=已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_class_date (classroom_id, use_date), KEY idx_date_slot (use_date, time_slot), CONSTRAINT fk_res_classroom FOREIGN KEY (classroom_id) REFERENCES classroom(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教室预约表';外键不要加ON DELETE CASCADE,因为教室一旦有关联预约记录时,应当阻止删除而不是静默连带删除,这样数据更安全。两个二级索引分别服务“查某教室某天的预约”和“查某天某时间段的预约”,都是高频查询路径。
2.3 初始化数据与状态枚举:先造几条能演示的数据
建好表后马上插入基础数据。开发阶段如果只有空表,条件筛选和状态切换都看不出效果。
INSERT INTO classroom (building, room_no, floor, capacity, media, status) VALUES ('1号教学楼', 'A101', 1, 80, 1, 0), ('1号教学楼', 'A102', 1, 60, 1, 1), ('2号教学楼', 'B201', 2, 120, 1, 0), ('实验楼', 'C301', 3, 50, 0, 2);这里故意让 A102 处于使用中、C301 处于维修状态,页面一打开就能看到不同状态下的按钮差异。设计报告里建议把“为何用 TINYINT 表示状态”作为一个小节写出,因为这是区分“调库完成”和“理解业务”的明显信号。
如果认为 admin 管理员的登录验证也要覆盖,再补一张最简单的用户表即可,密码在这类课设里先用明文或 MD5 存放,不要在本阶段引入 Spring Security 等复杂依赖。ER 图章节最终呈现为:admin(1)-(n)reservation(n)-(1)classroom,usage_log 作为可选扩展。
3. 项目结构与 JDBC 连接层:让 Java Web 项目和 MySQL 真正连通
数据库设计完成后进入 Java 工程。很多同学在这里卡住的原因不是不会写 SQL,而是不知道一个 Web 项目应该怎么分层、驱动 jar 放到哪、连接串参数怎么配。这一章给出一个课程设计最稳妥的工程结构,并且把连接层写得可以直接抄进项目。
3.1 为什么是 Servlet+JSP+JDBC,而不是 Spring Boot
课程设计和毕业设计写 Java Web 项目,最常见、最稳妥的做法是 Servlet+JSP+JDBC。Spring Boot 虽然启动快、开发爽,但它自动封装了太多底层细节,报告里很难讲清“数据库连接是如何建立的”。答辩老师问一句“你的连接池是什么,事务边界在哪”,没准备过的同学容易当场卡住。
手动搭一个 Servlet 项目时,依赖和分层都在自己手里,出错时也能顺着调用栈一步步查。推荐的包结构如下:
cims/ ├─ src/com/cims/entity/ Classroom.java, Reservation.java ├─ src/com/cims/dao/ ClassroomDao.java, ReservationDao.java ├─ src/com/cims/service/ ClassroomService.java, ReservationService.java ├─ src/com/cims/controller/ ClassroomListServlet.java 等 ├─ src/com/cims/util/ DBUtil.java ├─ web/WEB-INF/web.xml ├─ web/classroom_list.jsp, reservation_add.jsp ├─ lib/mysql-connector-java-8.0.30.jar └─ sql/init.sqlentity 放实体对象,dao 只写数据库访问,service 写业务判断,controller(即 Servlet)负责接收请求和跳转页面。在这个结构里,JSP 只做展示,不直接写 JDBC 代码。如果答辩时使用 Tomcat 运行,web.xml 里的 servlet 映射也可以统一管理路径。
3.2 数据库连接工具类 DBUtil 的最小可用代码
JDBC 连接不能每次都写一堆重复的DriverManager.getConnection,项目里统一抽一个工具类。这份代码适配 MySQL 8.0 和 JDK 8:
package com.cims.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/cims" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError( "MySQL 驱动类不存在,请检查 lib 下的 mysql-connector jar"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }连接串里的每个参数都有明确用途,这部分经常成为答辩时的加分点,整理如下:
| 参数 | 作用 | 不配置时出现的现象 |
|---|---|---|
| useUnicode=true&characterEncoding=utf8 | 让 Java 与 MySQL 之间按 UTF-8 交换数据 | 中文写入后变成问号或乱码 |
| serverTimezone=Asia/Shanghai | 指定 MySQL 服务器时区 | 报 SQLException: The server time zone value |
| allowPublicKeyRetrieval=true | 允许客户端获取 RSA 公钥,适配 MySQL 8 的 caching_sha2_password | 报 Public Key Retrieval is not allowed |
| useSSL=false | 本地开发关闭 SSL 握手 | 控制台刷大量 SSL 警告日志 |
PASSWORD 默认写死成 123456,这本项目里可接受。正式环境会把用户密码放到 properties 文件中,但课程设计阶段保持简单,把注意力放在 DAO 层。在静态代码块里使用Class.forName是为了确保驱动只注册一次;即使 JDBC 4.0 之后可以省略该行,保留它反而能让人一眼看出驱动加载过程。
3.3 本地起不来报错的三个高频原因
开发初期最常见的报错,都和 MySQL 环境或 jar 位置有关。建议维护一张排错对照表:
| 现象 | 原因 | 检查方式 |
|---|---|---|
| ClassNotFoundException: com.mysql.cj.jdbc.Driver | 驱动 jar 没放入 web/WEB-INF/lib | 打开项目 lib 目录确认文件存在 |
| Communications link failure | MySQL 服务没启动、端口被改或密码不对 | 执行 mysql -u root -p 测试命令行是否能进 |
| Access denied for user 'root'@'localhost' | 密码与连接串不一致 | 连接串换成命令行使用的正确密码 |
一个容易忽略的点是:很多 MySQL 8.0 安装教程默认把服务注册为MySQL80,不会开机自启。重新开机后 Java 项目先启动,MySQL 服务没起来,就会出现 Communications link failure。所以在写 Java 代码之前,老老实实用mysql -u root -p敲一遍密码,再执行一次SELECT 1,确认数据库本身能进,这才轮得上去排查 Java 侧。
连接层验证可用下面这段独立主函数,运行后能在控制台看到数据库名:
public class TestConn { public static void main(String[] args) { try (Connection conn = DBUtil.getConnection()) { System.out.println("连接成功,当前库:" + conn.getCatalog()); } catch (SQLException e) { e.printStackTrace(); } } }如果这一行输出正常,说明 MySQL 驱动、URL、用户名密码、网络端口四个环节全部打通。后面 Dao 的查询就只是 SQL 拼接和结果集处理了。
4. 教室增删改查和预约流程:把 DAO 与事务写进同一段代码
连接层就绪后,核心业务层的实现顺序建议是:先写教室的条件查询,再写新增修改,最后写预约和状态变更。预约会涉及两条表的同时更新,这正好是练习 JDBC 事务控制的机会。
4.1 教室列表的条件筛选查询实现
教室列表页面需要支持按楼栋模糊查询、按最低容量筛选、按状态查看。SQL 可以使用动态拼接,但必须用 PreparedStatement 占位符。
public List<Classroom> search(String building, Integer minCapacity) throws SQLException { StringBuilder sql = new StringBuilder( "SELECT id, building, room_no, floor, capacity, media, status" + " FROM classroom WHERE 1=1"); List<Object> params = new ArrayList<>(); if (building != null && !building.trim().isEmpty()) { sql.append(" AND building LIKE ?"); params.add("%" + building.trim() + "%"); } if (minCapacity != null && minCapacity > 0) { sql.append(" AND capacity >= ?"); params.add(minCapacity); } sql.append(" ORDER BY building, room_no"); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } try (ResultSet rs = ps.executeQuery()) { List<Classroom> list = new ArrayList<>(); while (rs.next()) { Classroom c = new Classroom(); c.setId(rs.getInt("id")); c.setBuilding(rs.getString("building")); c.setRoomNo(rs.getString("room_no")); c.setFloor(rs.getInt("floor")); c.setCapacity(rs.getInt("capacity")); c.setMedia(rs.getInt("media")); c.setStatus(rs.getInt("status")); list.add(c); } return list; } } }动态条件查询的惯用技巧是WHERE 1=1,这样后面的条件都能无脑拼接AND。在 Java 层判断参数为空,而不是拼一个AND building LIKE '%%',可以少传无效条件。当页面没有填入容量时,minCapacity 为 null,那么这个过滤条件就不会进入 SQL。使用 try-with-resources 后,Connection、PreparedStatement、ResultSet 都会自动关闭,避免连接泄漏。如果你的课程设计报告需要展示考虑点,可以补充这张表:
| 输入 | 生成的 WHERE 片段 | 效果 |
|---|---|---|
| building=空, capacity=空 | 无 | 返回全部教室 |
| building=博学 | AND building LIKE '%博学%' | 模糊匹配楼栋名 |
| capacity=80 | AND capacity >= 80 | 只显示容量足够的教室 |
| 两者都有 | 两个片段都追加 | 组合筛选 |
4.2 教室的添加与修改:预编译语句的六个参数绑定
新增和修改教室用的都是 INSERT 和 UPDATE 语句,区别只在最后一个参数:新增不需要 id,修改必须带上 WHERE id=?。在 Servlet 中接收表单后,调用 Service 的 save 方法,Dao 中执行预编译。
public int insert(Classroom c) throws SQLException { String sql = "INSERT INTO classroom(building, room_no, floor, capacity, media, status)" + " VALUES(?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, c.getBuilding()); ps.setString(2, c.getRoomNo()); ps.setInt(3, c.getFloor()); ps.setInt(4, c.getCapacity()); ps.setInt(5, c.getMedia()); ps.setInt(6, c.getStatus()); return ps.executeUpdate(); } }这里要注意 set 参数的类型顺序:String, String, int, int, int, int。报错大多来自column doesn't match value,本质是参数顺序写反。新增时 status 在业务层默认设为 0,而不是让用户填写,否则用户可以在表单里直接构造一个“已占用”的教室。修改场景类似,只需在 SQL 末尾追加WHERE id=?。
4.3 教室预约的冲突判断与事务回滚
预约模块是这个项目中最容易拉开分数的地方。预期业务是:用户选择日期、时间段、教室后提交,系统先检查该教室在该时段是否已经被预约,没有冲突才插入预约记录,同时把 classroom 状态更新为使用中。两条语句必须同时成功或同时失败,所以事务边界非常重要。
先查冲突,再插入并更新:
String checkSql = "SELECT COUNT(*) FROM reservation" + " WHERE classroom_id=? AND use_date=? AND time_slot=?" + " AND status IN (0,1)"; String insertSql = "INSERT INTO reservation(classroom_id, use_date, time_slot, purpose, applicant, phone, status)" + " VALUES(?,?,?,?,?,?,0)"; String updateClassSql = "UPDATE classroom SET status=1 WHERE id=?";Service 层按以下结构组织事务:
Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); try { long cnt = count(checkSql, classroomId, useDate, timeSlot); if (cnt > 0) { conn.rollback(); return "该教室在这个时段已被预约"; } insert(conn, insertSql, ...); update(conn, updateClassSql, classroomId); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); if (conn != null) { conn.close(); } }代码中status IN (0,1)的含义是,只要存在待审核(0)或已通过(1)的预约,就认为该时段冲突。已拒绝和已取消的记录不参与冲突判断。这就是事务最容易出错的地方:两条 update 必须共用一个 Connection,否则各自用新连接会自动提交,无法回滚。业务层的返回值"该教室在这个时段已被预约"可以直接响应给 JSP 页面,提示用户换时间。
预约状态流转可以用一张状态表说明,答辩时也方便展示:
| 状态值 | 含义 | 状态变更 SQL |
|---|---|---|
| 0 | 待审核 | 插入数据时默认 |
| 1 | 已通过 | UPDATE reservation SET status=1 WHERE id=? AND status=0 |
| 2 | 已拒绝 | UPDATE reservation SET status=2 WHERE id=? AND status=0 |
| 3 | 已取消 | UPDATE reservation SET status=3 WHERE id=? AND status IN (0,1) |
状态从 1 变为 3 时,还需要把对应的classroom.status改回 0,让教室重新显示为空闲。这个联动更新也应该放在同一事务里,和插入预约时的事务处理方式一模一样。
5. 答辩演示前,用一组脚本做最后的系统验证
开发完成后最尴尬的场景,不是功能没写完,而是演示时数据库连不上、页面中文乱码、刚录入的数据被前一轮测试清掉。答辩前留出 30 分钟做系统级验证,能提前暴露九成环境问题。
5.1 三分钟检查 MySQL 连接与数据完整性
实验室电脑、投影机下的笔记本,环境可能完全不同于开发机。演示前用命令行确认数据库状态,比点开浏览器更可靠:
mysql -u root -p cims -e \ "SELECT building, room_no, status FROM classroom ORDER BY id;" mysqladmin -u root -p ping第一条命令直接读取教室表,能同时确认连接密码、目标库、表名是否正确;第二条输出mysqld is alive表示 MySQL 服务健康。如果密码包含特殊字符,命令行里要注意转义,或者把这段命令写进批处理脚本。同时检查系统时钟,因为预约模块会用到当天的 use_date,电脑时间不对会导致“今天可预约的教室”列表为空。
5.2 编码验证与出错回滚机制
中文乱码多半出现在三层中的某一层:JSP 页面编码、Servlet 接收参数编码、JDBC URL 编码。除 URL 中已配置的characterEncoding=utf8外,还要在 Servlet 里做统一处理:
@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); chain.doFilter(req, resp); } }提示:
setCharacterEncoding必须在读取第一个请求参数之前执行,Filter 是全局安全的位置。
最后是数据回滚。演示时可能有人乱点测试,把数据改花,恢复手段比手动 DELETE 更可靠:
mysqldump -u root -p cims --single-transaction > backup_$(date +%Y%m%d).sql演示前跑一次备份,万一现场数据被改乱,执行mysql -u root -p cims < backup_xxx.sql即可还原。--single-transaction参数在 InnoDB 下导出时不会锁表,适合正在使用中的数据库。手动数据验证时,多查一句SELECT COUNT(*) FROM classroom WHERE status=1,确保页面有可展示的状态切换入口,比现场临时 insert 数据要从容得多。
本文还有配套的精品资源,点击获取