Java课设实战:影碟出租管理系统设计与避坑指南

发布时间:2026/10/18 12:03:10
Java课设实战:影碟出租管理系统设计与避坑指南
简介影碟出租管理系统是一份基于GUI/Swing与MySQL的Java课程设计源码包面向需要完成课设或练习桌面应用开发的高校学生。压缩包共49个文件体积约153KB包含8个Java源码文件覆盖登录、注册、添加、删除、修改、查询和JDBCUtils工具类等核心模块另有1份SQL数据库脚本、20个class编译文件、9张PNG界面截图及若干XML工程配置可导入常见Java IDE运行。资源中的源码与数据库脚本配套完整部署后即可启动系统界面风格贴近学生课设水平适合作为SwingMySQL项目的参考模板同时包含的编译产物和界面截图也有助于快速核对运行结果与界面布局。目前已有136人浏览学习对需要尽快完成课设或理解JDBC与GUI整合的同学具有实用价值。1. 影碟出租管理系统一个Java课设为什么值得认真做很多人把“影碟出租管理系统【GUI/SwingMySQL】Java课设”当成一个交差用的作业界面能弹出来、数据库能连上、增删改查点得动答辩一过就算完事。但恰恰是这类看起来“没什么技术含量”的课设最能暴露你对 Swing 事件模型、JDBC 资源管理和 MySQL 事务边界的真实理解。见过太多人在答辩现场翻车点“租借”按钮程序卡死、中文乱码、打包成 jar 后连不上数据库、两个人同时操作同一张碟片库存直接变负数。这些问题都不是“功能没写完”而是基础没夯实。这篇文章按一条能复现的路径来讲先理清这个课设到底在管什么数据、有哪些角色和状态流转再落到 Swing 界面和 MySQL 表结构的对接方式最后给出完整可抄的代码骨架和真正的坑点清单。适合正在做课设的学生也适合想用一个完整案例把 Java GUI JDBC 串起来复习一遍的开发者。只要你跟着把每一步跑通这就不只是一个“能交差的系统”而是一个能理直气壮写进简历的完整项目。2. 先把需求拆干净模块边界、角色权限和数据表设计2.1 影碟出租的业务本质是“状态流转”不是增删改查影碟出租管理系统表面上管的是“碟片信息”和“租借记录”但真正难做的不是 CRUD而是几个连续的状态流转碟片从“在架”变成“已租出”租借到期后变成“逾期”归还后回到“在架”。这一串状态如果不在设计阶段理清楚到了写代码的时候就会变成到处打补丁——在按钮事件里直接改库存数字、在表格里手工维护状态列、归还时忘记检查是否逾期。我一般会先把业务对象和状态画在一张纸上不急着开 IDE。这个系统里至少有这样的角色管理员负责维护碟片信息和查看所有租借记录普通用户或者叫会员可以查询碟片、发起租借和归还。对应到数据表最少需要四张碟片表、会员表、租借记录表和管理员表。碟片表里的核心字段是“库存总量”和“可借数量”后者才是界面上判断“能不能租”的依据租借记录表里必须同时存“应还日期”和“实际归还日期”否则“逾期”这个状态根本算不出来。这四张表的关系并不复杂一个会员可以有多条租借记录一条租借记录对应一张碟片的一个副本。难点在于租借动作同时要写两条数据——插入一条租借记录、减少碟片的可借数量——这两步必须放在同一个事务里。如果分开执行就会出现“记录写进去了但库存没扣”或者反过来的情况。这是后面写代码时最需要盯住的地方。2.2 表结构设计字段类型、默认值和唯一约束一次到位建表是课设里最不该省时间的环节。很多同学直接在 MySQL 里用图形工具点几个字段就开始写 Java 代码结果后面发现时间字段不知道用 DATE 还是 DATETIME、金额不想用 DOUBLE 但忘了还有 DECIMAL、会员手机号没加唯一约束导致重复注册。这些问题越早定下来后面改的代码就越少。我建议的字段设计思路是所有金额用 DECIMAL(10,2)所有日期用 DATE租借和归还只精确到天课设场景不需要时分秒所有主键用自增 INT 就行不用上雪花算法。会员表的手机号或者身份证号必须加 UNIQUE 约束这是业务上的硬要求。碟片表的“可借数量”默认值和“库存总量”保持一致避免出现“总量5张但可借6张”的脏数据。租借记录表的“实际归还日期”要允许 NULL因为一条未归还的记录这个字段就是空的。下面是建表 SQL直接粘贴到 MySQL 里就能用字段名和类型都按上面说的设计CREATE DATABASE IF NOT EXISTS video_rental DEFAULT CHARSET utf8mb4; USE video_rental; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE member ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL UNIQUE, reg_date DATE NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE disc ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, genre VARCHAR(30), total_count INT NOT NULL DEFAULT 1, available_count INT NOT NULL DEFAULT 1, price_per_day DECIMAL(10,2) NOT NULL DEFAULT 1.00 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE rental ( id INT PRIMARY KEY AUTO_INCREMENT, disc_id INT NOT NULL, member_id INT NOT NULL, rent_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE NULL, fee DECIMAL(10,2) DEFAULT 0.00, FOREIGN KEY (disc_id) REFERENCES disc(id), FOREIGN KEY (member_id) REFERENCES member(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有两个参数容易在课设里被忽略一个是建库语句里的DEFAULT CHARSET utf8mb4少了它后面插入中文碟名很可能报 Incorrect string value另一个是表引擎统一用 InnoDB因为租借动作要开事务MyISAM 不支持行级锁和事务回滚课设里用了 MyISAM 等于给自己埋雷。available_count这个字段是界面上判断能否租借的唯一依据建议在插入碟片时用total_count作为默认值避免两条数据对不上。2.3 模块划分界面层、业务层、数据访问层分开写课设最常见的代码组织方式是把所有东西塞进一个 JFrame 里按钮监听器里直接写 JDBC。这种写法在几百行代码时还勉强能跑一旦加上“查看租借记录”“统计逾期”这类功能一个类就会膨胀到上千行改一个字段要翻半天。我习惯按三层来组织不引入 Spring就用纯 Java 的包结构view包放所有 JFrame、JPanel、JTable 和事件监听器只负责界面展示和用户交互。service包放业务逻辑比如租借时同时写 rental 记录和更新 disc 库存逾期费用计算也在这里。dao包放所有 JDBC 数据访问代码每个表对应一个 DAO 类。这个分层的好处是出问题时能快速定位——界面卡顿查 view库存扣错查 serviceSQL 报错查 dao。答辩老师问你“这个项目怎么设计的”你能讲出这个结构比说“我用了 MVC 模式”更有说服力因为这是你自己代码里真实存在的划分。3. 代码落地从数据库连接到租借业务的最小可运行路径3.1 数据库连接用配置文件管理连接参数别硬编码写 JDBC 第一步是拿到 Connection。课设里最常见的写法是在每个 DAO 方法里DriverManager.getConnection(url, user, password)连接参数直接写在字符串里。这样做能跑但一旦换机器、换数据库账号就要改所有 DAO 类。更好的做法是写一个工具类把连接参数放在配置文件里启动时一次性加载。我习惯用一个DBUtil类里面有一个静态代码块负责加载驱动一个静态方法返回连接另一个静态方法负责关闭资源。这样 DAO 层只需要调用DBUtil.getConnection()代码干净很多。下面是完整的工具类写法package dao; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); Class.forName(com.mysql.cj.jdbc.Driver); url props.getProperty(jdbc.url); user props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception e) { e.printStackTrace(); } } } } }这里有两个关键点。第一Class.forName在 MySQL 8.x 的驱动下其实可以省略但保留它不会出错而且能让代码兼容 5.x 驱动课设环境里装了哪个版本都不必担心。第二close方法接收的是AutoCloseable...这样调用的时候可以一口气传rs, stmt, conn三个对象Java 会按传入顺序关闭资源——注意关闭顺序应该和打开顺序相反也就是先关 ResultSet再关 Statement最后关 Connection。db.properties文件放在src/main/resources目录下内容如下jdbc.urljdbc:mysql://localhost:3306/video_rental?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password你的密码URL 里的serverTimezoneAsia/Shanghai是 MySQL 8.x 连接时的必备参数不写会报 CST 时区相关的异常characterEncodingutf8保证中文数据在 Java 和 MySQL 之间传输不乱码。这两个参数就是课设里最常见的“玄学报错”来源后面避坑章节还会再提到。3.2 碟片管理的 GUI 实现JTable 数据显示和增删改查界面部分第一个核心页面是碟片管理。用JTable展示碟片列表通过DefaultTableModel来管理表格数据按钮事件里调用 DAO 做增删改查。先看数据访问层DiscDAO 里查所有碟片的方法package dao; import model.Disc; import java.sql.*; import java.util.ArrayList; import java.util.List; public class DiscDAO { public ListDisc findAll() { ListDisc list new ArrayList(); String sql SELECT id, title, genre, total_count, available_count, price_per_day FROM disc; try (Connection conn DBUtil.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { Disc d new Disc(); d.setId(rs.getInt(id)); d.setTitle(rs.getString(title)); d.setGenre(rs.getString(genre)); d.setTotalCount(rs.getInt(total_count)); d.setAvailableCount(rs.getInt(available_count)); d.setPricePerDay(rs.getBigDecimal(price_per_day)); list.add(d); } } catch (SQLException e) { e.printStackTrace(); } return list; } }注意这里用了try-with-resources语法Connection、Statement、ResultSet 都实现了 AutoCloseable方法执行完会自动关闭不用手动写 finally 块。这是 JDK 7 以后的标准写法课设里用这个语法能给代码加分也比手动 close 更安全。findAll方法返回ListDiscGUI 层拿到这个列表后填充到DefaultTableModel里。这里没有直接在 SQL 里用SELECT *而是显式列出了字段名原因是如果以后表结构加了字段SELECT *的列顺序可能和前端 JTable 列不对应排查起来很麻烦。GUI 层的碟片管理面板核心逻辑是点“添加”按钮时把表单数据插入数据库package view; import dao.DiscDAO; import model.Disc; import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.math.BigDecimal; import java.util.List; public class DiscPanel extends JPanel { private JTable table; private DefaultTableModel tableModel; private DiscDAO discDAO new DiscDAO(); public DiscPanel() { setLayout(new BorderLayout()); tableModel new DefaultTableModel( new Object[]{ID, 片名, 类型, 总库存, 可借数, 日租金}, 0); table new JTable(tableModel); add(new JScrollPane(table), BorderLayout.CENTER); JPanel btnPanel new JPanel(); JButton addBtn new JButton(新增碟片); addBtn.addActionListener(e - showAddDialog()); btnPanel.add(addBtn); add(btnPanel, BorderLayout.SOUTH); refreshTable(); } private void showAddDialog() { JTextField titleField new JTextField(10); JTextField genreField new JTextField(10); JTextField totalField new JTextField(10); JTextField priceField new JTextField(10); Object[] form {片名, titleField, 类型, genreField, 总库存, totalField, 日租金, priceField}; int result JOptionPane.showConfirmDialog(this, form, 新增碟片, JOptionPane.OK_CANCEL_OPTION); if (result JOptionPane.OK_OPTION) { Disc d new Disc(); d.setTitle(titleField.getText().trim()); d.setGenre(genreField.getText().trim()); d.setTotalCount(Integer.parseInt(totalField.getText().trim())); d.setAvailableCount(d.getTotalCount()); d.setPricePerDay(new BigDecimal(priceField.getText().trim())); discDAO.insert(d); refreshTable(); } } private void refreshTable() { tableModel.setRowCount(0); ListDisc discs discDAO.findAll(); for (Disc d : discs) { tableModel.addRow(new Object[]{ d.getId(), d.getTitle(), d.getGenre(), d.getTotalCount(), d.getAvailableCount(), d.getPricePerDay() }); } } }JOptionPane.showConfirmDialog是课设里最实用的 Swing 组件之一它能用几行代码就弹出一个包含多个输入框的对话框不用单独设计表单页面。参数是一个 Object 数组Swing 会自动把 String 当标签、把 JTextField 当输入框排列。总库存和可借数保持一致的逻辑在d.setAvailableCount(d.getTotalCount())这一行这是新增碟片时最容易遗漏的业务规则——如果忘了设置新碟片的可借数默认是 0界面上永远显示“不可借”。3.3 租借和归还一个事务里完成业务别在事件里裸写 JDBC租借业务是整个系统最核心的地方要同时做两件事插入一条租借记录把对应碟片的 available_count 减 1。两步必须在一个事务里完成任何一个失败都要回滚。我建议把这段逻辑放在 service 层而不是 GUI 层原因是事务控制代码和业务规则应该和界面解耦。下面是一个 RentalService 的完整实现package service; import dao.DBUtil; import java.sql.*; public class RentalService { public boolean rentDisc(int discId, int memberId, int days) { Connection conn null; PreparedStatement psInsert null; PreparedStatement psUpdate null; ResultSet rs null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); String checkSql SELECT available_count FROM disc WHERE id ? FOR UPDATE; PreparedStatement psCheck conn.prepareStatement(checkSql); psCheck.setInt(1, discId); rs psCheck.executeQuery(); if (rs.next() rs.getInt(available_count) 0) { conn.rollback(); return false; } rs.close(); psCheck.close(); String insertSql INSERT INTO rental (disc_id, member_id, rent_date, due_date) VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL ? DAY)); psInsert conn.prepareStatement(insertSql); psInsert.setInt(1, discId); psInsert.setInt(2, memberId); psInsert.setInt(3, days); psInsert.executeUpdate(); String updateSql UPDATE disc SET available_count available_count - 1 WHERE id ? AND available_count 0; psUpdate conn.prepareStatement(updateSql); psUpdate.setInt(1, discId); int rows psUpdate.executeUpdate(); if (rows 0) { conn.rollback(); return false; } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.close(psInsert, psUpdate, conn); } } }这段代码里有两个值得注意的点。第一个是SELECT ... FOR UPDATE它在事务内给碟片记录加了行级锁避免两个并发请求同时读到相同的 available_count 后都执行减一操作。第二个是更新库存时在 WHERE 条件里加上了available_count 0这保证了即使前面的检查被绕过UPDATE 也不会把库存减成负数这是课后题里“两个人同时租同一张碟片”的标准解法。setAutoCommit(false)之后到commit()之间的所有 SQL 都在同一个事务里。rollback()放在两个地方业务不满足时主动回滚SQL 异常时在 catch 块里回滚。注意 finally 里DBUtil.close只关了 psInsert、psUpdate 和 conn因为 psCheck 和 rs 在 try 块里已经手动关闭了这避免了资源重复关闭的问题。归还业务的逻辑正好相反把 available_count 加 1同时把 rental 表的 return_date 更新为当前日期再把逾期费用算出来。费用计算规则可以简单一点超过应还日期后按天收费费率取碟片的 price_per_day。这段逻辑放在 RentalService 的 returnDisc 方法里和 rentDisc 的结构基本一致只是 SQL 方向相反这里不贴完整代码核心 SQL 是UPDATE rental SET return_date CURDATE(), fee DATEDIFF(CURDATE(), due_date) * price_per_day WHERE id ? AND return_date IS NULL; UPDATE disc SET available_count available_count 1 WHERE id ?;注意第一个 UPDATE 的 JOIN 写法MySQL 里不能在 UPDATE 语句中直接引用另一张表的列所以要先查出 rental 记录的 disc_id 和 price_per_day再分别执行两个更新。DATEDIFF 返回负值说明没有逾期这时 fee 应该设成 0 而不是负数这个判断在 Java 层处理比写在 SQL 里更直观。4. 课设避坑五个让数据库和 Swing 黑匣子翻车的细节4.1 中文乱码加上问号字符集配置没做全现象界面和数据库里显示碟片名称都是“???”或者插入中文直接报 Incorrect string value 错误。原因字符集问题有三个可能的位置——数据库创建时没指定 utf8mb4、JDBC URL 没带 characterEncodingutf8、表字段的 collation 不是 utf8mb4。只改其中一个位置问题依然存在。解决把三层配置全部对齐。建库语句用DEFAULT CHARSET utf8mb4JDBC URL 里加characterEncodingutf8如果表已经建好了用ALTER TABLE disc CONVERT TO CHARACTER SET utf8mb4;修复。注意三个位置的字符集必须一致MySQL 8.x 默认字符集是 utf8mb4但 5.7 及以下版本默认可能是 latin1需要手动指定。4.2 本地连接正常打包 jar 后找不到驱动现象在 IDE 里运行一切正常导出 Runnable JAR 后点击查询按钮报ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因默认的 Eclipse 导出 Runnable JAR 选项“Extract required libraries”会把第三方 jar 包解压到当前 jar 里但如果驱动 jar 的签名文件没有排除掉Java 会认为签名被破坏而拒绝加载类。解决打包时选“Package required libraries into this JAR”而不是“Extract required libraries”或者用 Maven 的maven-assembly-plugin做 fat jar。如果实在想用 Extract需要在导出后把 jar 里 META-INF 下的*.SF、*.DSA、*.RSA文件全部删掉再运行。4.3 点按钮界面卡死耗时操作堵住了事件分发线程现象点击“查询租借记录”按钮整个窗口变成“无响应”状态等几秒才恢复数据量大时干脆像死机一样。原因Swing 的按钮监听器默认跑在 EDTEvent Dispatch Thread上而数据库查询是阻塞操作。查询期间 EDT 被占住所有界面绘制和点击事件都排队等待表现出来就是界面卡死。解决把数据库操作放到后台线程里用SwingWorker或者简单的new Thread()加SwingUtilities.invokeLater来更新界面。常见做法是new SwingWorkerListRental, Void() { Override protected ListRental doInBackground() { return rentalDAO.findOverdue(); } Override protected void done() { try { ListRental data get(); updateTable(data); } catch (Exception e) { e.printStackTrace(); } } }.execute();doInBackground里跑 JDBC 查询done方法里把结果填充到表格。这样查询期间界面能正常响应用户操作不会出现“白屏无响应”的尴尬局面。4.4 库存变成负数检查与更新之间没有锁现象两个用户同时租借同一张碟片两张租借记录都插入成功但碟片库存从 1 变成了 -1。原因先 SELECT 检查库存再 UPDATE 扣减不是一个原子操作。两个线程同时读到 available_count1都认为可以租然后各自执行减一最终得到 -1。解决在事务里用SELECT ... FOR UPDATE给碟片记录加锁或者直接用UPDATE disc SET available_count available_count - 1 WHERE id ? AND available_count 0这种带条件的更新语句然后用executeUpdate的返回值判断是否有行被更新为 0 就说明库存不够回滚事务。推荐后者因为代码更少且不需要额外加锁。4.5 SQL 语法看着没问题就是报错字段名撞上了保留字现象执行SELECT * FROM order报语法错误建表时name字段没问题但condition字段总是报错。原因MySQL 保留字表里包含了order、condition、desc等常用英文单词直接用它们当字段名或表名会被解析成关键字。解决建表时给表名和字段名加反引号包起来比如CREATE TABLE \order (...)或者在命名阶段就避开保留字把order改成orders、把condition改成status。这个坑通常在课设中期才暴露因为刚建表时数据量小、执行的 SQL 简单往往到写复杂查询时才触发。5. 答辩前值得做的三个加分改造5.1 给碟片表加一个“是否可租”的派生列让界面更友好数据表里的 available_count 是个数字但用户关心的其实是“这张碟片现在能不能租”。与其在 JTable 里裸显示数字不如在refreshTable时生成一列“状态”available_count 大于 0 显示“可租”等于 0 显示“已借完”小于 0 显示“数据异常”。这个改造不做任何 SQL 改动只是一行三元表达式但答辩时演示效果完全不同——老师看到的是一个会告诉你“能不能租”的系统而不是一个给你数字让你自己判断的工具。我一般还会加一个“是否逾期”的派生列在查询租借记录时判断 return_date 是否为空且 due_date 早于今天。这样的代码不需要额外查询在 DAO 层映射 Rental 对象时就计算好前端 JTable 直接显示。类似的细节改造能体现你对业务的理解比写一个“导出 Excel”功能更能打动答辩老师。5.2 用卡片布局管理多页面切换别再开一堆 JFrame很多课设的界面是点“碟片管理”弹出一个新窗口点“租借管理”又弹出一个新窗口关闭主窗口后这些子窗口还留着进程都不好退出。更合理的做法是用JFrame加CardLayout把碟片管理面板、租借面板、会员面板全部塞进同一个窗口通过左侧按钮或顶部菜单切换。CardLayout cardLayout new CardLayout(); JPanel cardPanel new JPanel(cardLayout); cardPanel.add(new DiscPanel(), disc); cardPanel.add(new RentalPanel(), rental); cardPanel.add(new MemberPanel(), member); JButton discBtn new JButton(碟片管理); discBtn.addActionListener(e - cardLayout.show(cardPanel, disc));这里的关键是CardLayout.show(Container, String)的第二个参数必须和add时传的字符串一致。这种做法的好处是单窗口、状态共享简单——比如租借面板需要选择一个会员可以直接从主窗口持有的 MemberPanel 里取数据不用跨窗口传值。另外整个应用只有一个 JFrame关闭窗口时进程能正常退出不会出现线程不结束的情况。5.3 给租借按钮加确认对话框和操作结果反馈租借动作是对数据的修改而且涉及两张表误操作后要手动改数据库才能恢复。我的习惯是在执行前弹一个确认框int confirm JOptionPane.showConfirmDialog(this, 确认将碟片《 discTitle 》租借给会员 memberName , 租借确认, JOptionPane.YES_NO_OPTION); if (confirm ! JOptionPane.YES_OPTION) { return; }确认框的好处不只是防止误操作它还是你向答辩老师展示“我在关键操作上做了保护”的证据。执行完之后再用 JOptionPane 提示成功或失败——成功提示“租借成功请于 X 月 X 日前归还”失败提示“库存不足或该碟片暂不可租”。这两个弹窗加起来不到十行代码但会让整个交互显得完整而不是那种“点一下按钮表格就变了也不知道成没成功”的临时作品。最后说一个自己踩过的教训课设做完以后记得把数据库从自己机器搬到另一台干净环境上跑一遍。很多人只在开发机器上验证过换一台电脑就出现驱动版本对不上、数据库没初始化、账号密码不对等一系列问题。提前把db.properties的配置改成相对通用的值写好一份建库脚本放进项目根目录答辩演示时就能从容很多。希望帮到你。本文还有配套的精品资源点击获取