简介:一份基于Java开发的简单电影管理系统源码,整合了JSP、Servlet、JavaScript、CSS、HTML及PHP等Web技术,适合Java初学者、课程设计或小型电影资料库管理场景。系统覆盖电影信息录入、查询、管理和展示等核心功能,支持电影海报与简介展示,前后端结构完整,能帮助读者理解JavaWeb项目从页面交互到数据库操作的完整链路。压缩包共236个文件,大小约16.45MB,包含Java源码、JSP页面、SQL脚本、JS脚本、CSS样式、字体图标及大量PNG图片素材,目录按Web工程常见结构组织,便于对照学习。目前已有301人学习下载。作为完整可运行的源码工程,既可用作毕业设计或实训参考,也能在其基础上扩展搜索、评分、评论等模块,是快速上手JavaWeb开发的实用素材。
1. 基于Java的简单电影管理系统:课设源码到底要写成什么样
「基于 Java 的简单电影管理系统」大概是 Java 课程设计里出现频率最高的题目之一,网上的设计源码也最多,但质量参差:有的只有两个类硬凑,有的把 SQL 直接写在按钮监听里,拷下来根本跑不起来。
这个项目真正要交付的,是一套能现场演示的登录、电影增删改查、模糊搜索和订单记录,外加一份讲得清楚的分层代码结构。
这篇笔记按我实际交付课设源码的路径来讲:选型、建表、DAO 层、界面联动,再到验收前最容易翻车的五个地方。适合正在做 Java 课设、或者想拿这个项目练 JDBC 与 Swing 的人,照着能复现,也能应付答辩追问。
2. 技术选型和项目骨架:为什么 Swing + JDBC 是课设的稳妥答案
拿到题目先别急着写代码。电影管理系统可用的技术栈其实不少,但标题里「简单」两个字已经把范围框死了。我的判断是 Swing + MySQL + JDBC:界面用桌面窗口,数据落 MySQL,JDBC 做桥。这套组合最大的优点是老师拿到源码后不需要装 Tomcat、不需要配 Node,只要 JDK 和 MySQL 就能跑;缺点也明显,界面观感偏老,但课设阶段完全够用。
2.1 三种常见技术栈的取舍:课设不是越新越好
先看对比,再选型:
| 技术栈 | 上手门槛 | 演示环境要求 | 代码量 | 答辩风险 |
|---|---|---|---|---|
| Swing + JDBC + MySQL | 低 | JDK 8+、MySQL | 中 | 低,原理好讲 |
| JavaFX + JDBC | 中 | JDK 8 或额外装 OpenJFX | 中 | 中,版本坑多 |
| JSP/Servlet + Tomcat | 中 | JDK、Tomcat、MySQL | 偏高 | 中,环境问题多 |
| Spring Boot + Vue | 高 | JDK、Maven、Node | 高 | 高,明显超出课设范围 |
JavaFX 在 JDK 8 里还是官方组件,但从 JDK 11 开始被剥离,需要单独引 OpenJFX 依赖,很多同学在这一步就卡住了。JSP/Servlet 需要部署到 Tomcat,一旦端口被占用或者 web.xml 写错,演示现场就很难收场。Spring Boot + Vue 当然更好看,但对「简单电影管理系统」来说属于过度设计,答辩时老师一句「这里为什么不用 JDBC 直连」就能把节奏带偏。
所以我一般建议老老实实用 Swing。它虽然不那么现代,但事件监听、布局管理、表格模型这些知识点都是 Java 基础课上的核心内容,和标题里的「简单设计源码」定位完全匹配。常见做法是:登录窗口一个 JFrame,主界面一个 JFrame,里面用 JTabbedPane 放电影管理、订单查询两个面板,数据展示用 JTable。也别急着去网上下载现成源码改,那些源码里充斥着乱码、驱动版本过时、没有分层的问题,改起来比自己写还费劲。
2.2 项目目录分层:实体、DAO、Service、UI 四层怎么分
网上能搜到的 Java 课程设计案例源码里,分层混乱是通病。很多只有一个 Main 类加一个 Connection 工具类,数据库操作全写在按钮的 ActionListener 里。这种写法跑通很容易,但答辩时几乎必被问「如果我要加一个会员表,你的代码要改几个地方」。所以哪怕项目再简单,我也建议按四层组织:
movie-system/ ├── src/ │ └── com/course/movie/ │ ├── entity/ Movie.java User.java Order.java │ ├── dao/ MovieDao.java UserDao.java OrderDao.java │ ├── service/ MovieService.java UserService.java OrderService.java │ ├── ui/ LoginFrame.java MainFrame.java │ └── util/ DBUtil.java MD5Util.java ├── sql/ │ └── init.sql # 建库建表 + 样例数据 └── lib/ └── mysql-connector-java-8.0.33.jarentity 里只放字段和 getter/setter,对应数据库表;dao 只写 SQL,一个方法对应一条语句;service 做业务判断,比如价格不能为负、库存不能超卖;ui 只负责界面和事件,不直接拼 SQL。这样分层之后,面向对象编程的好处才体现得出来:数据、逻辑、展示互相解耦,哪一层出问题就只改哪一层。
一个容易忽略的细节是包名。我见过不少源码把类直接放在默认包下,这在命令行编译时会很痛苦,而且没法用包级私有访问。从第一天就用com.course.movie这种带反域名规则的包名,之后无论是打 jar 还是写测试都顺很多。实体类的字段尽量和数据库列名一一对应,下划线转驼峰,减少 DAO 层里手动映射的出错概率。
2.3 依赖与运行环境:JDK 版本、MySQL 驱动和兼容性
如果课程允许用 Maven,pom.xml 里只需要一个运行时依赖:
<dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>如果老师要求不联网、全程命令行演示,那就下载驱动 jar 放进 lib 目录,编译运行靠 classpath 指进来:
javac -encoding UTF-8 -cp "lib/*;src" src/com/course/movie/ui/Main.java java -cp "lib/*;src" com.course.movie.ui.Main这里有个非常典型的坑:MySQL 5.x 和 8.x 的驱动类名不一样。MySQL 5.x 用com.mysql.jdbc.Driver,MySQL 8.x 必须用com.mysql.cj.jdbc.Driver。很多老源码里还写着旧类名,连 MySQL 8 时直接报 ClassNotFoundException,这不是代码错了,是版本没对齐。
驱动 jar 的版本不要求和 MySQL 服务端完全一致,但 5.1.x 的旧驱动连 MySQL 8 会遇到caching_sha2_password认证报错,反过来 8.x 驱动连 5.7 倒是常见没问题。所以最简单的方案是:统一用 8.0.x 驱动,不管服务端是 5.7 还是 8.0。JDK 方面建议用 8 或 11,Swing 在 8 上最稳,11 也兼容,但别用太新的 JDK 版本去跑跨大版本的课设代码,编译选项经常会引入额外问题。
提示:如果开发环境没联网,提前把驱动 jar 拷到 lib 目录,并在 IDEA 的 Project Structure -> Modules -> Dependencies 里把它标记为 Library,否则 IDEA 里能编译、命令行里
java -cp却找不到类。
3. 数据库设计与 JDBC 封装:把电影表的基础操作一次写对
技术骨架定下来之后,真正的工程量在数据库和 DAO 层。这个环节决定了一件事:后面界面写起来是顺滑还是到处补丁。我的习惯是先把 SQL 脚本写完、跑通,再写 Java 代码,因为 DAO 里的每个方法都是一条 SQL 的映射,SQL 设计烂了,Java 层怎么补都别扭。
3.1 建表 SQL:电影、用户、订单三张表的最小设计
「简单」不等于只建一张电影表。一个能上台面的电影管理系统至少要有:电影表、用户表、订单表。用户表解决登录,订单表把“谁、在什么时候、买了哪部电影”记下来,这样答辩时才有东西可讲。
CREATE DATABASE IF NOT EXISTS movie_system DEFAULT CHARACTER SET utf8mb4; USE movie_system; CREATE TABLE t_movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, genre VARCHAR(30), director VARCHAR(50), release_date DATE, duration INT DEFAULT 120, price DECIMAL(6,2) NOT NULL DEFAULT 0, stock INT NOT NULL DEFAULT 0, description VARCHAR(500) ); CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password CHAR(32) NOT NULL, role VARCHAR(20) DEFAULT 'STAFF' ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, order_time DATETIME NOT NULL, CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_order_movie FOREIGN KEY (movie_id) REFERENCES t_movie(id) );几个字段设计上的说明:price用 DECIMAL 而不是 DOUBLE,因为浮点数存金额会出现 0.1 + 0.2 不等于 0.3 的精度问题,这在财务相关字段上是原则性的;release_date用 DATE 类型,Java 侧对应java.sql.Date,后面避坑章会专门讲它和java.util.Date的转换;stock是库存余量,下单时减一,这个字段是后面事务演示的抓手。password CHAR(32)对应 MD5 的 32 位十六进制输出,如果后面改用 SHA-256 就要改成 CHAR(64)。
外键在课设里建议保留。虽然性能上有争议,但老师看到表结构里有外键和索引,至少知道你有数据库设计的意识。样例数据我一般会插 5 部不同类型的电影,年份拉开、价格有高有低,这样演示「按类型筛选」「按价格排序」时不用现场现改数据。插入样例时注意先插入 t_user,再插入 t_movie,最后插入 t_order,否则外键约束会直接报错。
3.2 DBUtil 封装:连接 URL 那一串参数到底在防什么
JDBC 连接代码是网上下载源码里问题最多的部位,最大的问题不是语法,而是把连接参数写死在每个方法里。我见过的课设里,十个有八个在每次查询时重新DriverManager.getConnection,窗口一开就打开了几十个连接,数据库连接数直接被耗尽。正确做法是封装一个 DBUtil:
package com.course.movie.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/movie_system" + "?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 RuntimeException("MySQL 驱动加载失败,请检查 lib 目录", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }静态代码块保证驱动类只加载一次。URL 后面四个参数每个都有实际意义:characterEncoding=utf8配合建库时的utf8mb4,解决中文乱码;useSSL=false关闭 SSL 握手,本地开发能省掉一堆证书告警日志;serverTimezone=Asia/Shanghai解决 MySQL 8 报的The server time zone value错误,这个报错信息在中文系统里经常变成乱码,新手很容易误判成驱动问题;allowPublicKeyRetrieval=true是 MySQL 8 用caching_sha2_password插件时客户端首次连接需要的参数,不加会报Public Key Retrieval is not allowed。
这里顺带说明白一个 Java 基础考点:Class.forName的作用是触发驱动类的静态初始化,8.x 驱动其实在 JDBC 4.0 之后支持 SPI 自动注册,不写这行也能工作,但课设里写上更稳妥,也方便被问到「驱动是怎么加载的」时有个标准答案。
3.3 MovieDao 核心方法:PreparedStatement 的参数和返回值
DAO 层里电影表的增删改查是整套源码的主干。以添加和条件查询为例:
public int addMovie(Movie m) throws SQLException { String sql = "INSERT INTO t_movie(title, genre, director, release_date, " + "duration, price, stock, description) VALUES(?,?,?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, m.getTitle()); ps.setString(2, m.getGenre()); ps.setString(3, m.getDirector()); ps.setDate(4, new java.sql.Date(m.getReleaseDate().getTime())); ps.setInt(5, m.getDuration()); ps.setBigDecimal(6, m.getPrice()); ps.setInt(7, m.getStock()); ps.setString(8, m.getDescription()); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { return rs.next() ? rs.getInt(1) : -1; } } } public List<Movie> findMovies(String keyword) throws SQLException { String sql = "SELECT * FROM t_movie WHERE title LIKE ? ORDER BY id DESC"; List<Movie> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Movie m = new Movie(); m.setId(rs.getInt("id")); m.setTitle(rs.getString("title")); m.setGenre(rs.getString("genre")); m.setDirector(rs.getString("director")); m.setReleaseDate(rs.getDate("release_date")); m.setPrice(rs.getBigDecimal("price")); m.setStock(rs.getInt("stock")); list.add(m); } } } return list; }代码里有三个值得在答辩时展开的点。第一,PreparedStatement用?占位并在setXxx时指定类型,除了防 SQL 注入,还能避免字符串拼接时引号转义的麻烦;第二,RETURN_GENERATED_KEYS让我们在插入后拿到自增主键,这是订单表关联电影时必须的能力;第三,try-with-resources会自动关闭 Connection、Statement、ResultSet,传统写法里忘记关闭 ResultSet 会导致连接泄漏,泄漏到一定程度就会出现「系统跑着跑着突然全部请求超时」的诡异现象。
rs.getDate("release_date")返回java.sql.Date,而实体类里如果用java.util.Date声明,赋值时建议像代码里那样在 set 时显式转换,或者实体类直接声明成java.sql.Date,能少掉一半转换代码。模糊查询的LIKE参数写法是"%" + keyword + "%",占位符只传一个拼接好的字符串,不要自己在外层拼 SQL 再传进去,那样又回到了注入的坑里。
3.4 Service 层:业务校验与下单事务,顺带解决数据一致性
网上源码大多只有 DAO 和界面两层,Service 通常是空的。但加上一个薄薄的 Service 层,代码档次马上不一样。它负责 DAO 不负责的事:校验、事务、业务规则。以「用户购票下单」为例,这个动作要同时做两件事:往 t_order 插一条记录,把 t_movie 的 stock 减一。两步必须同时成功或同时失败,否则会出现「订单记录了但库存没扣」的数据不一致。
public boolean orderMovie(int userId, int movieId, int quantity) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 try (PreparedStatement ps1 = conn.prepareStatement( "INSERT INTO t_order(user_id, movie_id, quantity, order_time) VALUES(?,?,?,NOW())")) { ps1.setInt(1, userId); ps1.setInt(2, movieId); ps1.setInt(3, quantity); ps1.executeUpdate(); } try (PreparedStatement ps2 = conn.prepareStatement( "UPDATE t_movie SET stock = stock - ? WHERE id = ? AND stock >= ?")) { ps2.setInt(1, quantity); ps2.setInt(2, movieId); ps2.setInt(3, quantity); if (ps2.executeUpdate() == 0) { conn.rollback(); // 库存不足,回滚订单 return false; } } conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { } return false; } finally { try { if (conn != null) conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { } } }这段代码直接用一条带stock >= ?条件的 UPDATE 来防超卖,比先 SELECT 再 UPDATE 更安全,因为它在数据库层面保证了原子性。setAutoCommit(false)之后,两条 SQL 共享同一个 Connection 上的事务,任何一条失败就 rollback。这个写法在「java 怎么保证数据一致性」这类问题下是标准答案之一:要么用事务,要么用数据库约束兜底。
提示:事务治理的关键是 Connection 必须从外面传进来,不能在每个 DAO 方法内部各自 getConnection。否则两条 SQL 各开各的连接,事务根本不存在。常见做法是 Service 层获取连接、开启事务、把 Connection 作为参数传给 DAO 方法。
4. 登录鉴权与界面联动:把登录框到主界面的链路一次打通
数据库和 DAO 层完成后,系统的核心能力已经有了,接下来是界面。这一章最容易被低估,因为很多人以为就是拖控件。实际上课设答辩时,老师点开程序第一个操作就是登录,登录进不去,后面全白搭。而且登录不是简单比对用户名密码,还牵涉到密码存储、会话传递、权限控制三个问题。
4.1 密码存储:MD5 加盐而不是明文比对
用户表的 password 字段设计成 CHAR(32) 就是为了存 MD5 摘要。登录校验时,把用户输入的密码做一次 MD5,再和库里存的值比对。这样即使数据库文件被拷走,密码原文也不会直接泄露。
public static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); // 每个字节补成两位十六进制 } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }这里用String.format("%02x", b)把每个字节转成两位十六进制,拼出来正好 32 位。要注意input.getBytes()不指定编码时依赖平台默认字符集,跨系统部署可能出现同一密码摘要不同的怪事,所以固定用 UTF-8。登录查询的 SQL 是SELECT * FROM t_user WHERE username=? AND password=?,配合 PreparedStatement 占位符,就把经典的' or '1'='1注入挡在门外。
提示:MD5 已经不算安全加密算法,容易被彩虹表碰撞,课设里为了讲清楚原理可以用。如果想让答辩多一个亮点,可以在 MD5 前加固定盐:
md5(username + password + "movie_salt"),并在初始化管理员账号时用同样的规则生成摘要。
4.2 主界面与表格刷新:JTable 数据不更新的根因
主界面最核心的组件是 JTable。常见做法是给 JTable 配一个 DefaultTableModel,把数据库里查出来的 List 转成二维数组填进去。很多源码在这个环节翻车:点完「新增」按钮后数据库里确实多了一行,但界面纹丝不动,必须重启程序才能看到。
private void refreshTable(String keyword) { try { List<Movie> movies = movieService.findMovies(keyword); String[] columns = {"ID", "片名", "类型", "导演", "上映日期", "时长", "票价", "库存"}; Object[][] rows = new Object[movies.size()][columns.length]; for (int i = 0; i < movies.size(); i++) { Movie m = movies.get(i); rows[i][0] = m.getId(); rows[i][1] = m.getTitle(); rows[i][2] = m.getGenre(); rows[i][3] = m.getDirector(); rows[i][4] = m.getReleaseDate() == null ? "" : m.getReleaseDate().toString(); rows[i][5] = m.getDuration(); rows[i][6] = m.getPrice(); rows[i][7] = m.getStock(); } table.setModel(new DefaultTableModel(rows, columns)); // 整体替换模型触发重绘 } catch (SQLException e) { JOptionPane.showMessageDialog(this, "查询失败:" + e.getMessage()); } }关键在最后一行的setModel:JTable 的视图模型被整体替换,界面才会触发重绘。如果只改数据源而不重新 setModel,JTable 根本不知道数据变了。建议把这段逻辑收成一个refreshTable(String keyword)方法,所有按钮的事件回调里统一调用它。另一个相关细节是,DefaultTableModel 默认不允许编辑单元格,如果想让双击单元格直接改库存,需要重写isCellEditable方法返回 true,否则界面看得到改不了。
4.3 事件监听与窗口跳转:登录状态如何带到主界面
登录成功后,要做的事情不是简单new MainFrame().setVisible(true),而是把当前登录用户传过去。常见做法是给 MainFrame 加一个带 User 参数的构造器:
loginBtn.addActionListener(e -> { String username = userField.getText().trim(); String password = new String(passField.getPassword()); User user = userService.login(username, password); if (user == null) { JOptionPane.showMessageDialog(LoginFrame.this, "用户名或密码错误"); return; } LoginFrame.this.dispose(); // 释放登录窗口 new MainFrame(user).setVisible(true); // 把登录用户带进主界面 });这里有两个容易被答辩追问的细节。第一,密码框要用JPasswordField而不是 JTextField,用getPassword()返回 char 数组而不是字符串,这是 Swing 的安全惯例,虽然课设里很多人嫌麻烦直接用 JTextField。第二,登录成功后用dispose()而不是setVisible(false)关闭登录窗口,否则 JVM 里残留的隐式窗口会让程序退出时卡在后台,任务管理器里看到 java 进程一直不消失。
权限控制可以在 MainFrame 构造器里做:根据user.getRole()判断「管理员」还是「员工」,管理员显示删除和订单管理按钮,员工只能查询和购票。这是答辩的加分点,也是业务系统里最基础的权限模型。具体实现只要一行deleteBtn.setVisible("ADMIN".equals(user.getRole())),不需要引入任何框架。
4.4 订单查询面板:关联查询的结果映射
订单面板要展示的不只是 t_order 表本身,而是订单、用户名、电影名三张表 join 后的结果。SQL 写成:
SELECT o.id, u.username, m.title, o.quantity, o.order_time FROM t_order o JOIN t_user u ON o.user_id = u.id JOIN t_movie m ON o.movie_id = m.id ORDER BY o.order_time DESC;这属于 Java 基础的 JDBC 结果映射练习:ResultSet 里同时存在 u.username 和 m.title,字段名不会冲突,直接用rs.getString("username")和rs.getString("title")取值即可。唯一要注意的是如果 SQL 里有重名字段(比如两张表都有 id),必须在 SELECT 里用别名区分,否则rs.getInt("id")会取到不确定的一列。这个面板建议放在 JTabbedPane 的第二个页签里,和电影管理页签分开,界面逻辑更清晰。
5. 避坑记录:验收前最容易翻车的五个地方
这一章全部是血泪经验。前四章的东西照着写都能跑,但课设演示崩掉的几乎都是下面这几类问题。每一条都按「现象 → 原因 → 解决」的顺序写,方便在交付前自己排查一遍。
5.1 中文乱码:界面和数据库里全是问号
现象:程序能跑,但界面上的标题、数据库里的电影名全是 ?? 或乱码,控制台输出中文也偶尔错位。
原因:三个环节的字符集不一致——Java 源码文件编译编码、JDBC 连接字符集、数据库表字符集。最常见的组合是源码用 GBK 保存、连接串没写 characterEncoding、表又是 latin1 默认字符集。
解决:把三处统一成 UTF-8。IDEA 里 Settings -> File Encodings 改成 UTF-8;MySQL 建库用DEFAULT CHARACTER SET utf8mb4;JDBC URL 加characterEncoding=utf8。命令行编译时加-encoding UTF-8。改完这三处,乱码基本绝迹。建库时如果用 Navicat 手工建库,记得把字符集下拉框从默认改成 utf8mb4,排序规则选 utf8mb4_general_ci 即可。
5.2 ClassNotFoundException:驱动类找不到
现象:点登录按钮时抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者直接 NoClassDefFoundError。
原因:驱动 jar 没在运行 classpath 里。IDEA 里编译通过但运行时找不到,通常是 jar 只在编译期被引用,没有作为 Library 加入模块依赖;命令行运行时则是没写-cp lib/mysql-connector-java-8.0.33.jar。
解决:IDEA 里 File -> Project Structure -> Modules -> Dependencies 添加 jar;命令行用java -cp "lib/*;target/classes" com.course.movie.ui.Main的写法,lib/*会把目录下所有 jar 都加进来。另外检查驱动类名,MySQL 8 用com.mysql.cj.jdbc.Driver,老源码里的com.mysql.jdbc.Driver在 8.x 包里已经被移除。
5.3 JTable 不刷新:新增、删除后界面纹丝不动
现象:数据库里数据已经变了,JTable 还是显示旧数据,重启程序才正常。
原因:只是改了内部数据源,没有通知 JTable 重新绘制。DefaultTableModel 的数据变化需要触发 table 结构更新,或者干脆整体替换 model 对象。
解决:统一走refreshTable()方法,内部调用table.setModel(new DefaultTableModel(rows, columns))。如果用的是tableModel.addRow()这种增量写法,记得在批量操作后调用model.fireTableDataChanged(),否则 UI 线程不知道数据变化,事件通知机制没生效。
5.4 日期转换:java.util.Date 和 java.sql.Date 的类型冲突
现象:实体类用java.util.Date声明 releaseDate,DAO 层ps.setDate报编译错误;或者界面显示日期变成带时分秒的一长串。
原因:PreparedStatement.setDate只接受java.sql.Date,而实体类里导入的全是java.util.Date,两个类都叫 Date,写代码时很容易引错包。反过来从 ResultSet 读出的 sql.Date 直接塞进 util.Date 字段,行为诡异但能编译。
解决:实体类字段直接声明成java.sql.Date,或者 DAO 层显式转换:new java.sql.Date(m.getReleaseDate().getTime())。显示时统一date.toString()输出 yyyy-MM-dd 格式,不要额外拼格式化逻辑。如果哪天改成 LocalDate,就要配套用rs.getObject("release_date", LocalDate.class),这套新 API 会干净很多。
5.5 SQL 注入:输入' or '1'='1直接登录成功
现象:用户名输入admin' or '1'='1,密码随便填,居然登录成功了。
原因:登录 SQL 是字符串拼接:"SELECT * FROM t_user WHERE username='" + username + "' AND password='" + password + "'"。输入内容把 SQL 的引号闭合,再用恒真条件绕过密码校验。Statement 拼 SQL 的写法天然有这个问题,不光是登录,任何查询都可能被注入。
解决:全部改用 PreparedStatement 参数占位。这不仅是安全修复,还能让代码可读性变好。如果老师追问「怎么从源头防范」,答案就三条:参数化查询、输入校验(username 限制长度和字符集)、数据库账号最小权限。课设里做到第一条就足够,但能把三条都说出来,答辩印象分会高不少。
6. 验收演示与两个加分扩展:从「跑通」到「敢答辩」
系统能跑起来只是及格线,验收现场才是真正的考验。我的经验是:不要等老师随机点,自己设计一条有逻辑的演示路径,把每个功能都走到,同时把数据库的坑提前填平。
6.1 十分钟验收流程:按这个顺序演示最稳
我一般按下面这个顺序走,每步都有明确的预期结果:先故意输错一次密码,确认弹「用户名或密码错误」,证明校验生效;再用管理员账号登录,切换到员工账号登录,确认删除按钮被隐藏,证明权限控制生效;然后新增一部电影,表格立刻出现新记录;接着用关键字模糊搜索,确认结果即时变化;最后以员工账号下单一部电影,订单面板出现记录且库存减一。这套流程覆盖了增、删、改、查、登录、权限、订单、关联查询,全部走完大概十分钟。演示前一天把数据库里的样例数据整理干净,不要留一堆测试垃圾数据,老师最怕看到第一步点开电影列表,屏幕上蹦出十几行「测试1」「test2」。
6.2 加分扩展:导出电影清单与简单的票房统计
两个改动小、见效快的扩展方向。第一个是导出:用 Apache POI 把电影列表写成 Excel 文件。
try (Workbook wb = new HSSFWorkbook()) { Sheet sheet = wb.createSheet("电影清单"); Row header = sheet.createRow(0); header.createCell(0).setCellValue("片名"); header.createCell(1).setCellValue("导演"); header.createCell(2).setCellValue("票价"); for (int i = 0; i < movies.size(); i++) { Row row = sheet.createRow(i + 1); row.createCell(0).setCellValue(movies.get(i).getTitle()); row.createCell(1).setCellValue(movies.get(i).getDirector()); row.createCell(2).setCellValue(movies.get(i).getPrice().doubleValue()); } wb.write(new FileOutputStream("movie_list.xls")); }不想引入 POI 依赖的话,用 BufferedWriter 输出 CSV 也一样能交差,Excel 打开时识别逗号分隔即可。第二个扩展是统计面板,写一条SELECT genre, COUNT(*) FROM t_movie GROUP BY genre的聚合 SQL,用 JTable 展示;再进一步按订单量算热门电影:SELECT m.title, SUM(o.quantity) FROM t_order o JOIN t_movie m ON o.movie_id=m.id GROUP BY m.title ORDER BY SUM(o.quantity) DESC。这两条 SQL 是答辩时「系统有什么业务价值」这个问题的最好回答。
6.3 交付源码前最后一遍检查
我自己交付课设源码前有个固定习惯:删掉所有测试输出和临时文件,把数据库初始化脚本跑一遍全新环境,确认从建库到登录全流程无人工干预;再检查一遍 lib 目录,确保驱动包在、版本号和代码里的类名对得上;最后把 README 里写清楚 JDK 版本、MySQL 版本、启动顺序和默认账号密码。这套源码从「让我跑起来」变成「让老师跑起来」,才是真正交付完成。希望这些选型和踩坑记录能帮到你,少熬两个通宵。
本文还有配套的精品资源,点击获取