开发指南)
简介这是一份面向Java课程设计的影碟出租管理系统源码采用GUI/Swing界面配合MySQL数据库适合正在完成课设或需要快速搭建可运行Demo的高校学生。资源包共49个文件压缩包大小仅153KB内容涵盖Java源代码、数据库初始化脚本、编译后的class文件、xml工程配置以及多张png界面运行截图其中java文件实现登录、注册、增删改查、全量查询等核心业务sql脚本可直接导入MySQL完成建库建表xml与class文件便于在IDE中恢复工程结构png图片可用于核对界面效果。系统模块划分清晰代码风格克制朴素整体来自实际交付过的课设项目运行可靠性较高。目前已有136人学习下载并附带作者整理的详细部署指引与售后支持能帮助读者避开环境配置的常见坑点节省调试时间。对于希望低成本完成Java课设并顺利演示答辩的同学是一份完整且实用的参考资源。1. 影碟出租管理系统到底要做什么从课设题目到可运行方案期末课设拿到「影碟出租管理系统【GUI/SwingMySQL】Java课设」这个题目时大多数人第一反应是找现成代码改一改。真正要面对的问题是无论代码从哪来你都得能讲清楚 Swing 界面怎么组织、MySQL 表怎么设计、Java 的 JDBC 怎么把两边接起来——这三件事恰好也是答辩时老师最爱追问的点。这个题目本质是一个标准的 Java 课设用 Swing 做桌面 GUI用 MySQL 存数据中间靠 JDBC 连接覆盖了增删改查、外键关联、模糊查询和简单报表难度适中适合用来验证 Java SE 和数据库基础。这篇笔记的目标是带你从建表开始一条命令一条代码地把系统搭出来顺便把那些容易让人翻车的细节提前摆到桌面上。2. 先把数据模型定住MySQL 表结构与业务字段选择不少课设项目翻车不是代码写不出来而是表结构拍脑袋定的写界面时发现字段不够用或者租碟逻辑根本没法表达。做 Swing 界面之前先把数据模型稳住后面所有 Panel 和 DAO 都是围着这几张表转的。2.1 从出租业务倒推需要哪几张表影碟出租的核心业务是谁、在哪天、租了哪张碟约定什么时候还实际什么时候还收了多少钱。顺着这个流程倒推最少需要三张表碟片表、会员表、出租记录表。碟片表要回答“这张碟的库存和状态”所以除了标题、导演、分类这些描述字段还必须有一个总库存和一个当前可借数量。会员表要记录联系方式同时保存押金余额——现实中租碟一般要先交押金逾期从押金里扣。出租记录表是业务的核心流水租出日期、应还日期、实际归还日期、租金、状态都放在这条记录里。这里有个常见设计分歧要不要把归还记录单独拆一张表我一般不建议拆。课设场景下一次出租对应一次归还拆成两张表会让查询要多做一次 JOIN还会引入“一张出租记录对应零或一条归还记录”这种 1:0..1 关系徒增复杂度。租赁的完整生命周期用一条记录加状态字段就够了在借、已还、逾期。这也简化了后续 Swing 表格的展示逻辑——一行数据就是一条完整的业务流程。2.2 建表 SQL字段类型与状态约束的取舍下面这三张表是我在课设项目里常用的结构字段命名保持简单方便和 Java 实体类映射。CREATE DATABASE IF NOT EXISTS dvd_rental DEFAULT CHARSET utf8mb4; USE dvd_rental; CREATE TABLE disk ( disk_id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL COMMENT 碟片标题, director VARCHAR(50) COMMENT 导演, category VARCHAR(20) COMMENT 分类动作/科幻/剧情等, release_year YEAR COMMENT 发行年份, per_day_rate DECIMAL(4,2) DEFAULT 2.00 COMMENT 每日租金, stock INT NOT NULL DEFAULT 1 COMMENT 总库存, available_stock INT NOT NULL DEFAULT 1 COMMENT 当前可借数量, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架, 0下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE member ( member_id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 会员姓名, phone VARCHAR(20) NOT NULL UNIQUE COMMENT 手机号唯一, id_card VARCHAR(18) COMMENT 身份证号用于押金登记, deposit DECIMAL(7,2) DEFAULT 50.00 COMMENT 押金余额, registered_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE rental ( rental_id INT AUTO_INCREMENT PRIMARY KEY, disk_id INT NOT NULL, member_id INT NOT NULL, rent_date DATETIME NOT NULL COMMENT 租出时间, due_date DATETIME NOT NULL COMMENT 应还时间, return_date DATETIME NULL COMMENT 实际归还时间, rental_fee DECIMAL(7,2) DEFAULT 0 COMMENT 实际收取租金, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在借, 2已还, 3逾期, FOREIGN KEY (disk_id) REFERENCES disk(disk_id), FOREIGN KEY (member_id) REFERENCES member(member_id), INDEX idx_rent_status (status), INDEX idx_due_date (due_date) ) ENGINEInnoDB;几个参数上的取舍值得展开说明。release_year 用 YEAR 类型而不是 INT是因为 YEAR 在 MySQL 里就能限制范围界面下拉框或日期选择器拿到的年份也正好对应这它。金额一律用 DECIMAL 而不是 FLOAT浮点型算押金扣款会出现 0.30000000000000004 这类误差DECIMAL(7,2) 能精确到分。status 用 TINYINT 而不是 VARCHAR 存“在借/已还/逾期”既省空间又方便在 Java 里用 switch 映射成中文如果存字符串一旦 someone 打错字比如“在借”写成“在借、”查询时就查不出来。外键和索引在课设量级下不是性能问题但加上它们能让老师觉得你的表设计是经过考量的。2.3 用测试数据验证表结构后补关联字段的教训表建完先别急着写 Java用几条 INSERT 把逻辑走一遍确认“可借数量”这个字段能自洽。INSERT INTO disk (title, director, category, release_year, per_day_rate, stock, available_stock) VALUES (盗梦空间, 诺兰, 科幻, 2010, 3.00, 5, 5), (活着, 张艺谋, 剧情, 1994, 2.00, 3, 3), (让子弹飞, 姜文, 动作, 2010, 2.50, 4, 4); INSERT INTO member (name, phone, id_card, deposit) VALUES (张伟, 13800001111, 110101199001011234, 100.00), (李娜, 13900002222, 110101199205023456, 50.00); -- 模拟一次租碟张伟租走一张《盗梦空间》 UPDATE disk SET available_stock available_stock - 1 WHERE disk_id 1; INSERT INTO rental (disk_id, member_id, rent_date, due_date, status) VALUES (1, 1, NOW(), DATE_ADD(NOW(), INTERVAL 3 DAY), 1);执行这几条 SQL 后重点观察两点一是《盗梦空间》的 available_stock 从 5 变成 4说明库存扣减逻辑通顺二是 rental 表里的 due_date 自动算成了 3 天后说明 DATE_ADD 这类日期函数可以放心在 DAO 里用。这里暴露过的一个问题是有人先建了表、写完了 Java 实体类才发现 rental 表里没有 rental_fee 字段又回头 ALTER TABLE 加列实体类、DAO、界面表格全部跟着改一遍非常被动。所以建表时宁可先多考虑业务流程也不要吝啬那几个字段。测试数据里把身份证号写死是为了演示唯一约束实际课设可以用更简单的假数据。3. Swing 界面怎么搭主窗口、表格与弹窗代码骨架直接抄数据库就绪之后进入 GUI 部分。Swing 本身不难难在界面的组织方式——很多课设代码把 JDBC 查询直接写在按钮监听器里最后黄成一大坨改一个需求要动三个地方。3.1 界面分层为什么别把 SQL 写在按钮事件里Swing 界面代码天然臃肿new JButton、setBounds、addActionListener 这些代码会占据大量行数。如果业务逻辑也混在里面整个类能上千行而且没法测试。常见做法是分三层界面层放 Swing 组件负责显示和收集输入DAO 层放 JDBC 代码负责操作数据库中间可以再用实体类POJO作为数据载体。按钮点击事件里只做一件事从界面拿参数调 DAO得到结果刷新表格。这样每个类职责单一答辩时被问“你这个查询是怎么实现的”也能直接把 DAO 里的 SQL 翻出来讲而不是在监听器里大海捞针。3.2 主窗口骨架JFrame JTabbedPane 的代码模板主窗口用 JFrame内部用 JTabbedPane 切换“碟片管理”“会员管理”“租还管理”三个页。这个布局的好处是天然区分模块而且每个 Tab 的子面板可以独立成类互不干扰。import javax.swing.*; import java.awt.*; public class MainFrame extends JFrame { public MainFrame() { setTitle(影碟出租管理系统); setSize(960, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JTabbedPane tabPane new JTabbedPane(); tabPane.addTab(碟片管理, new DiskPanel()); tabPane.addTab(会员管理, new MemberPanel()); tabPane.addTab(租还管理, new RentalPanel()); setContentPane(tabPane); } public static void main(String[] args) { SwingUtilities.invokeLater(() - { new MainFrame().setVisible(true); }); } }main 方法里那层 SwingUtilities.invokeLater 不是可有可无的装饰。Swing 组件必须在事件调度线程EDT上创建和操作不定时它直接 new MainFrame()在 Windows 上可能看不出问题但换到某些 Linux 桌面环境偶发组件绘制不全。用 invokeLater 把窗口创建任务丢到 EDT 事件队列里是 Swing 入门的第一个好习惯。JTabbedPane 的参数这里用了默认的换页标签位置实际用的时候如果标签文字较多可以改成 setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT) 让标签可滚动。setSize(960, 600) 对现代屏幕偏小但作为课设演示足够不要为了撑画面盲目调大。3.3 表格刷新不踩坑JTable 与 DefaultTableModel 的联动每个 Tab 面板里最常见的组件就是 JTable。碟片列表、会员列表、出租记录都是表格展示。Swing 里 JTable 本身不存数据它依赖 TableModel最常用的是 DefaultTableModel。import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.util.List; public class DiskPanel extends JPanel { private final JTable table new JTable(); private final DefaultTableModel model new DefaultTableModel( new Object[]{ID, 标题, 分类, 可借数量, 状态}, 0 ); public DiskPanel() { setLayout(new BorderLayout()); table.setModel(model); JScrollPane scrollPane new JScrollPane(table); add(scrollPane, BorderLayout.CENTER); refreshTable(); } private void refreshTable() { model.setRowCount(0); // 清空旧数据避免重复追加 ListDisk list new DiskDao().listAll(); for (Disk d : list) { model.addRow(new Object[]{ d.getDiskId(), d.getTitle(), d.getCategory(), d.getAvailableStock(), d.getStatus() 1 ? 上架 : 下架 }); } } }这里有两个细节需要解释。setRowCount(0) 是刷新表格的关键如果不清空每次刷新数据都会在旧行下面追加表格行数翻倍增长。状态字段在界面层转成中文而不是在 SQL 里 CASE WHEN把展示逻辑留在视图层SQL 保持简洁这个分工在后续做“按状态筛选”时会方便很多。DefaultTableModel 的构造参数里第一个是列名数组第二个是初始行数 0。refreshtable() 方法在增删改操作后都要调用一次。如果新增碟片后表格不更新基本就是忘了调这个方法。这个联动提一下操作成功的回调里调 refreshTable()是整个 GUI 项目里出现频率最高的代码之一。4. JDBC 数据访问层从连接参数到事务控制的完整写法界面有了表格接下来用 JDBC 把界面和 MySQL 接通。这一章是整个系统能不能跑起来的生命线也是问题最多的地方。4.1 DBUtil连接参数这样配才不出乱码和时区报错所有 DAO 都依赖数据库连接所以第一步是把连接逻辑抽成工具类。很多课设项目在这里踩坑MySQL 8 的驱动类名和 5.7 不同时区不指定启动就报错连接串不带编码参数导致中文乱码。import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/dvd_rental ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; 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 RuntimeException(MySQL驱动未找到, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }URL 里每个参数都有故事。useUnicodetrue 和 characterEncodingutf8 保证中文数据写入和读取时编码一致没加这两个参数时界面输入中文保存后MySQL 里看到的是一串问号。useSSLfalse 是关闭加密连接本地开发不需要 SSL 握手否则 MySQL 8 会打印一堆 SSL 警告。serverTimezoneAsia/Shanghai 是 MySQL 8 的硬性要求因为驱动要拿到数据库时区才能转换 DATETIME 类型不设置会直接抛异常。allowPublicKeyRetrievaltrue 配合 MySQL 8 的 caching_sha2_password 认证插件使用不设置时可能报连接失败。用户名密码直接写死在类里是课设的常见偷懒做法能跑但不够好。如果想让老师眼前一亮把这三项挪到 config.properties 里用 java.util.Properties 读取代码也就多十行。驱动类名用 com.mysql.cj.jdbc.Driver这是 MySQL Connector/J 8.0 及以后版本的类名如果你的机器上装的是老驱动会用到 com.mysql.jdbc.Driver这两个别搞混了避坑章会细说。4.2 DAO 写法PreparedStatement 与 ResultSet 映射数据库连接类有了接下来写一个最基础的 DAO查询碟片列表和新增碟片这是所有业务的基座。import java.sql.*; import java.util.ArrayList; import java.util.List; public class DiskDao { public ListDisk listAll() { String sql SELECT disk_id, title, category, available_stock, status FROM disk ORDER BY disk_id; ListDisk list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Disk d new Disk(); d.setDiskId(rs.getInt(disk_id)); d.setTitle(rs.getString(title)); d.setCategory(rs.getString(category)); d.setAvailableStock(rs.getInt(available_stock)); d.setStatus(rs.getInt(status)); list.add(d); } } catch (SQLException e) { throw new RuntimeException(查询碟片列表失败, e); } return list; } public int addDisk(Disk d) { String sql INSERT INTO disk (title, director, category, release_year, per_day_rate, stock, available_stock) VALUES (?, ?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, d.getTitle()); ps.setString(2, d.getDirector()); ps.setString(3, d.getCategory()); ps.setInt(4, d.getReleaseYear()); ps.setBigDecimal(5, d.getPerDayRate()); ps.setInt(6, d.getStock()); ps.setInt(7, d.getStock()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(新增碟片失败, e); } } }两个关键点。一是必须用 PreparedStatement 而不是 Statement 拼字符串。用户输入的电影标题里如果包含单引号拼 SQL 的方式会直接语法错误甚至被 SQL 注入PreparedStatement 用 ? 占位符由驱动做转义既安全又省去手动拼接引号的麻烦。二是 try-with-resources 语法Connection、PreparedStatement、ResultSet 都会自动关闭避免忘了关连接导致数据库连接数耗尽。addDisk 里新增碟片时把 stock 同时赋给 available_stock因为新碟不存在借出。这里注意 release_year 用 setInt 传入Java 实体类里对应 Integer数据库 YEAR 类型正好能接住。4.3 租碟/还碟为什么要开事务一个操作更新三张表租碟这个动作表面上一个按钮背后涉及两步数据库操作扣减碟片的 available_stock插入一条新的 rental 记录。两步中间只要任何一步失败就会出现“库存扣了但没租出去记录”或者“有记录但没扣库存”的脏数据。这个问题用事务解决。public void rentDisk(int diskId, int memberId, int days) { String updateStock UPDATE disk SET available_stock available_stock - 1 WHERE disk_id ? AND available_stock 0; String insertRental INSERT INTO rental (disk_id, member_id, rent_date, due_date, status) VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY), 1); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement psStock conn.prepareStatement(updateStock)) { psStock.setInt(1, diskId); int rows psStock.executeUpdate(); if (rows 0) { throw new SQLException(库存不足或碟片不存在); } } try (PreparedStatement psRental conn.prepareStatement(insertRental)) { psRental.setInt(1, diskId); psRental.setInt(2, memberId); psRental.setInt(3, days); psRental.executeUpdate(); } conn.commit(); } catch (SQLException e) { try (Connection conn DBUtil.getConnection()) { conn.rollback(); } catch (SQLException ex) { throw new RuntimeException(回滚失败, ex); } throw new RuntimeException(租碟失败, e); } }这段代码有几个值得说的设计。setAutoCommit(false) 把自动提交关掉让两条 SQL 在同一个事务里任何一条抛出异常rollback 会把前一条已经执行的影响撤销。updateStock 里加了 AND available_stock 0 做条件这是防止超卖的关键如果库存已经是 0UPDATE 影响行数为 0直接抛异常而不是让库存变成负数。这个“影响行数判断”是很多课设项目遗漏的细节不加这个判断两次并发租同一张碟库存会变成 -1。还碟逻辑是镜像操作UPDATE rental SET return_dateNOW(), status2 WHERE rental_id? 加上 UPDATE disk SET available_stock available_stock 1 WHERE disk_id?同样要放在事务里。5. 课设避坑指南连不上数据库、中文乱码、界面卡死的真实原因这一章写给那些“代码照着敲完了运行就是不对”的时刻。以下几条是我见过的课设高频问题每条都有明确的现象和对应的解决动作。5.1 现象程序启动就抛 ClassNotFoundException错误信息带 mysql 字样原因基本只有一个驱动 JAR 没导进项目依赖。在 IDEA 里即使你把 mysql-connector-java 的 jar 放进了项目的 libs 目录如果没右键 Add as Library运行时就找不到类。另外驱动的类名如果写成了 com.mysql.jdbc.Driver而你用的是 MySQL 8 的驱动包新驱动已经换成了 com.mysql.cj.jdbc.Driver老类名在新版本里被移除了。解决确认 IDE 中项目结构里引入的是 mysql-connector-j 的 jar确认 DBUtil 里 Class.forName 的类名和驱动包版本匹配。最简单的验证方法是看驱动包内部是否有该类解压 jar在 META-INF 目录或直接查类路径能搜到就是路径配对了。5.2 现象连接报 Communications link failure 或 Access denied for user这两类错误别混在一起分不清。link failure 是网络/服务层面的问题MySQL 服务没启动、主机名或端口写错、3306 被占用。Access denied 是认证问题用户名密码错、账号没有远程访问权限。解决第一步在命令行或 Navicat 里确认 MySQL 服务确实是 running 状态用 net start mysqlWindows或 systemctl status mysqlLinux查看。第二步确认连接 URL 里 localhost 和 3306 与 MySQL 实际监听地址一致。第三步如果账号密码都对还是 Access denied在 MySQL 里执行 ALTER USER rootlocalhost IDENTIFIED BY 密码; 刷新权限这通常能解决 8.0 版本里认证插件引起的连接困惑。5.3 现象界面输入中文保存到数据库变成 ? 或乱码这个是课设重灾区而且往往不是单一原因。第一层是连接串URL 少了 characterEncodingutf8驱动与 MySQL 之间的连接就不是 UTF-8。第二层是数据库和表的字符集建库时如果 DEFAULT CHARSET 是 latin1后面所有表都是 latin1存中文必变乱码。第三层是 IDEA 控制台的输入编码控制台默认 GBK 和项目的 UTF-8 冲突。解决从源头逐步排查。先看库和表的 charset执行 SHOW CREATE TABLE disk确认 CHARSETutf8mb4。然后把连接串参数补全参考第 4 章的 URL。最后在 IDEA 里把 VM options 加 -Dfile.encodingUTF-8 并重启确保界面输入的字本来就是 UTF-8。这三步都做了乱码还出现的话检查数据库连接工具同步的编码设置但大概率是这三层之一。5.4 现象点查询按钮后界面卡死几秒才恢复数据量大时甚至白屏Swing 是单线程模型所有事件处理都在 EDT 上执行。你直接在最简单的查询方法里执行 JDBC 操作数据库响应需要几百毫秒这期间界面就无法响应鼠标和键盘感觉像是卡死。解决在按钮监听器里不要直接做耗时操作使用 SwingWorker 把 JDBC 查询放到后台线程拿到结果后再切回 EDT 更新表格。课设数据量通常不大如果只是演示阶段很多人不做这一步也能交差但只要涉及跨网络访问数据库这是必改项。一个折衷办法是用 SwingWorker 包一下 DAO 调用doInBackground 里查数据done 里刷新表格代码量不大但界面手感完全不一样。6. 答辩前值得做的一个改进借阅排行报表与事务回滚演示系统功能都跑通之后还有一步能让答辩印象分明显提升做一个借阅排行报表并在演示时主动展示一次脏数据被回滚的过程。6.1 一条 SQL 让报表有数据可讲在“租还管理”页面加一个“借阅排行”的按钮和表格区域用 GROUP BY 统计每张碟的出租次数就能回答“哪种碟最受欢迎”这个课设高频提问。SELECT d.title, COUNT(r.rental_id) AS rent_times FROM rental r JOIN disk d ON r.disk_id d.disk_id GROUP BY d.title ORDER BY rent_times DESC LIMIT 10;这条 SQL 的价值在于它同时用到了 JOIN、聚合函数、分组和排序是老师最喜欢的“你讲讲这条查询的原理”的素材。执行计划层面rental 表的外键索引会加速 JOIN数据量不大时性能没问题。落成 Java 代码时只需要在 DAO 里加一个 listRentalRanking() 方法返回一个 ListMapString, Object 或直接用一个简单的统计实体再在 RentalPanel 里用 JTable 展示两列碟片标题和出租次数。6.2 演示节奏的经验先证伪再显真答辩演示时主动设计一个“失败场景”比一路顺利更能体现你对系统的理解。可以这样演示故意用超级管理员的界面创建一个新会员然后租一张 available_stock 已经为 0 的碟片让系统抛出“库存不足”→ 再查数据库确认 rental 表没有多出记录disk 表库存没有变成负数。这直接验证了你的事务实现是真的有效而不只是贴了代码。演示之前先在 MySQL 里把测试数据清理干净DELETE 掉调试过程产生的记录让表里的主键和真实业务看起来一致。我吃过一次亏演示时屏幕上出现了调试用的“测试1”“测试2”数据老师当场问“这条记录是什么业务产生的”场面瞬间尴尬。事后习惯是任何课设项目提交前统一重置测试数据保留三到五条看起来像真实业务的数据就好。希望这些经验帮到你让答辩时少一个坑多一分从容。本文还有配套的精品资源点击获取