ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Java Swing与MySQL的停车场管理系统设计与实现

基于Java Swing与MySQL的停车场管理系统设计与实现 简介基于Java Swing与MySQL的停车场管理系统面向Java初学者及课程设计、毕业设计等场景覆盖用户注册登录、车辆入场/出场登记、车辆信息查询与删除等核心操作。项目采用JDK1.8与MySQL8.0.13环境数据库设计仅admin与car两张表结构清晰易懂。资源包为RAR压缩格式共49个文件包含12个Java源文件、31个编译后class文件、1个SQL数据库脚本以及project、prefs、classpath、jar等工程配置文件压缩包大小1.96MB并提供运行截图便于预览界面效果。目前已有467人学习使用。借助这套资源可快速获得完整可运行的源码与数据库建表脚本免去从零搭建环境的时间适合用来练习Swing图形界面设计、JDBC数据库交互也可作为二次开发或改造的基础模板。1. 停车场管理系统 2用 Java Swing 和 MySQL 做一套桌面收费系统到底值不值得做每年课程设计和毕业设计Java 方向几乎人手一个“某某管理系统”。这个标题里的“2”可能是第二次迭代也可能是老师题号里的编号不管哪种本质都一样用 Swing 搭窗口用 MySQL 存业务数据。停车场系统比一般的图书管理、学生管理多了一次“计时收费”的业务闭环所以常被拿来当课程设计题目。它能解决的是小型停车场的人工登记混乱问题谁的车几点进场、几点出场、该收多少钱、哪些车位空着全部落库可查。适合刚学完 Java 基础、想完整走一遍 CRUD 的人也适合想把课设改造成面试作品集的 Java 初学者。2. 为什么选 Java Swing MySQL桌面信息系统的适配边界与功能模块怎么拆2.1 Swing 没有被淘汰只是退到了合适的场景Swing 是个有年头的老牌 GUI 工具包谈不上先进但稳定。很多人一听 Swing 就皱眉觉得这个技术不值一提。我一般会反问一句你的系统跑在哪停车场的场景是门卫室一台普通电脑、内网访问不需要给外部用户并发访问。Swing 做桌面端最大的优势是部署简单JRE 装好双击 JAR 包就起来不用装 Tomcat不用处理前后端跨域也不用考虑浏览器兼容性。MySQL 作为共享数据库放在局域网另一台机器或同一台机器上几个窗口共享同一套数据这正好命中信息管理系统的最小需求。用这个组合还有一个现实原因Java 基础课程里Swing 和 JDBC 往往是最后两个章节学生不需要额外学 Spring、MyBatis 就能把系统拼出来。这也决定了项目的复杂度边界界面代码负责交互JDBC 负责读写业务逻辑写在 service 或 dao 里。不要在这个项目里强行塞微服务、消息队列那才是本末倒置。2.2 功能模块按“谁来用”拆管理员、收费员、普通查询停车场系统看上去只是进出实际上至少有三类用户角色不一样看到的界面和操作权限也不一样。管理员负责配置收费规则、查看每日营收、管理车位和账号收费员在岗亭里执行车辆进场、车辆出场、现金收费和手工查询普通查询角色可能只给老板看报表不让改数据。对应到功能模块上可以拆成五块登录与账号管理、车位管理、车辆进出登记、收费规则配置、记录查询与统计。每块里再往下拆比如车位管理要支持新增车位、把某个车位设为维修停用、查看空车位数量车辆进出登记要处理月卡用户和临时车记录查询要支持车牌模糊搜索、按日期汇总。不要贪多先把这五块跑通再考虑停车券、会员折扣。这个拆法还有一个好处你写简历或答辩时能清楚讲出每个角色对应的权限和功能而不是笼统说“我做了一个系统”。2.3 核心数据模型没有好表就写不好业务数据模型直接决定后续代码好不好写。我见过不少人一开始只建一张“车辆进出表”结果月卡、收费规则全都塞在同一个表里后期改需求改到怀疑人生。常见做法是拆成五张表用户表、车位表、停车记录表、收费规则表、月卡表。停车记录表和车位表关联临时车和月卡车都走同一张记录表用状态字段区分在场和已出场。表名核心字段说明t_userid, username, password, role, real_name登录用户与角色t_parking_spaceid, space_no, area, type, status车位唯一编号和占用状态t_car_recordid, plate_no, space_id, enter_time, exit_time, fee, status, operator每次进出形成一条记录t_fee_ruleid, rule_name, base_price, unit_minutes, unit_price, free_minutes, max_daily_fee临时车收费规则t_monthly_cardid, plate_no, owner_name, begin_date, end_date, status月卡有效期一般情况下老客户按月卡走进场时只判断卡是否在有效期内临时车则按收费规则计算费用。两张表不混在一起后面算费用和做月卡到期提醒都会轻松很多。表之间不要建太多外键尤其不要在 Swing 项目里依赖数据库外键级联业务校验放到 Java 代码里更好排查问题。3. 建库建表与 JDBC 连接从 MySQL 装好到查到第一条记录3.1 MySQL 安装完成后的三件事建库、授权、设置字符集新装的 MySQL 先别急着写代码。第一步用 root 登录第二步创建一个单独的业务库第三步如果不想所有地方都用 root 连可以单独建一个账号并授权。很多新手在“连接不上”这个问题上翻车不是 Java 代码的问题而是库没建、账号密码不对、或者驱动版本和 MySQL 版本不匹配。下面这段 SQL 可以直接执行。注意建表语句里的 COMMENT 和 ENGINE我一般会把每个字段的用途写在注释里方便自己两个月后回来看也方便答辩时讲表结构。CREATE DATABASE IF NOT EXISTS parking2 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE parking2; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT 课程设计可存MD5生产环境建议加盐, role VARCHAR(20) NOT NULL DEFAULT cashier, real_name VARCHAR(50) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_parking_space ( id INT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(10) NOT NULL UNIQUE COMMENT 车位编号如A01, area VARCHAR(20) DEFAULT A区, type VARCHAR(10) DEFAULT 普通 COMMENT 普通/新能源/残障, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1占用 2维修, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位表; CREATE TABLE t_car_record ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, space_no VARCHAR(10) DEFAULT NULL COMMENT 冗余车位编号方便查询, enter_time DATETIME NOT NULL, exit_time DATETIME DEFAULT NULL, fee DECIMAL(10,2) DEFAULT 0.00, status VARCHAR(10) NOT NULL DEFAULT IN COMMENT IN在场 OUT已出场, operator VARCHAR(50) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_plate (plate_no), KEY idx_enter_time (enter_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT停车记录表; CREATE TABLE t_fee_rule ( id INT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(50) NOT NULL, base_price DECIMAL(10,2) NOT NULL DEFAULT 5.00 COMMENT 起步价, unit_minutes INT NOT NULL DEFAULT 30 COMMENT 超起步时间后按多少分钟计费, unit_price DECIMAL(10,2) NOT NULL DEFAULT 2.00 COMMENT 每个计费单位的价格, free_minutes INT DEFAULT 15 COMMENT 免费时长, max_daily_fee DECIMAL(10,2) DEFAULT 30.00 COMMENT 24小时封顶价 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收费规则表; CREATE TABLE t_monthly_card ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL UNIQUE, owner_name VARCHAR(50) DEFAULT NULL, begin_date DATE NOT NULL, end_date DATE NOT NULL, status TINYINT DEFAULT 1 COMMENT 1有效 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT月卡表;建表时我特意把 t_car_record 的 time 字段建了索引因为后面最常用的查询就是“今天进出多少车”和“某个车牌的历史记录”没有索引的表在数据量大了以后会明显变慢。这里还有一个细节停车记录表里冗余了 space_no而不是只存 space_id。因为车位可能被重新规划冗余一个编号可以避免两个表 join 才能显示车位号查询时省一次关联。库和表建好后往 t_parking_space 里插入几十个模拟车位往 t_user 里插入一个管理员账号。建议用明文先跑通流程之后再把密码改成加密。插入语句我就不单独贴了代码里写死测试数据也可以。如果是命令行导入 SQL 文件注意用mysql --default-character-setutf8mb4 -u root -p parking2 schema.sql避免中文注释和表数据乱码。3.2 通过 JDBC 连接 MySQLJdbcUtil 工具类表有了下一步就是让 Java 能拿到连接。JDBC 连接 MySQL 的方式很固定最基础的做法是写一个 JdbcUtil 工具类把 URL、用户名、密码集中管理再提供 getConnection 和 close 方法。这里用到的驱动类是com.mysql.cj.jdbc.Driver这是 MySQL Connector/J 8.x 的驱动类名如果你用的是 5.x 老驱动类名是com.mysql.jdbc.Driver两者不要混。package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcUtil { // 连接参数直接写在代码里只适合本地开发下节讲每个参数的作用 private static final String URL jdbc:mysql://localhost:3306/parking2 ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8 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 ExceptionInInitializerError(MySQL驱动类未找到请检查jar包是否引入); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement st, java.sql.ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (st ! null) { try { st.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码的逻辑很清楚静态代码块在类第一次加载时注册驱动getConnection 每次从 DriverManager 拿一个物理连接close 方法按照 ResultSet、Statement、Connection 的顺序反向关闭。一定要先关 ResultSet 再关 Statement最后关 Connection否则在 MySQL 端会留下未释放的游标连接池不够用的时候会报“Too many connections”。参数说明放在一个小表里更清楚参数作用建议值useSSLfalse本地连接不需要 SSL 加密避免 SSL 握手报错本地开发用 falseserverTimezoneAsia/Shanghai指定服务器时区解决 8 小时时间差Asia/ShanghaicharacterEncodingutf8Java 和 MySQL 之间传输中文用 UTF-8utf8allowPublicKeyRetrievaltrue允许客户端向 MySQL 8 服务端获取公钥配合 caching_sha2_password 用户本地开发用 true写完这个类可以写一个 main 方法测试连接。不要继续往下写界面先跑通SELECT 1再说不然到时候分不清是数据库问题还是界面问题。public static void main(String[] args) throws SQLException { Connection conn JdbcUtil.getConnection(); java.sql.Statement st conn.createStatement(); java.sql.ResultSet rs st.executeQuery(SELECT 1); if (rs.next()) { System.out.println(连接成功); } JdbcUtil.close(conn, st, rs); }这段测试代码如果正常打印“连接成功”说明 JDBC 链路已经打通。如果报错直接看异常信息里的第一个英文句子不要只看最后一行。常见错误有Access denied账号密码错、Unknown database库名错、Server time zone时区参数缺失、Public Key RetrievalallowPublicKeyRetrieval 未设置。这些问题百分之九十都能通过调整 URL 和账号权限解决。3.3 连接参数不是玄学时区、SSL、公钥获取一次讲清很多同学第一次遇到 MySQL 8 连接报错时会在网上搜到一堆“玄学”解决办法改 URL、换成老版本驱动、给 root 改密码规则等。其实这几个参数背后都有明确原因。serverTimezone是因为新版 JDBC 驱动要求服务器时区必须明确否则它拿不到系统时区useSSLfalse是因为 MySQL 8 默认开启 SSL 支持但本地开发没有配置证书SSL 握手就会耗时间甚至失败allowPublicKeyRetrievaltrue是因为 MySQL 8 默认用户插件是caching_sha2_password如果客户端用的旧版驱动第一次连接时服务端不会直接下发公钥需要客户端主动请求。这里我不会把所有责任都丢给参数。如果你用的是 MySQL 5.7caching_sha2_password不存在allowPublicKeyRetrieval也就不是必须的。最稳妥的做法是在建库授权时创建一个专门给业务用的账号密码规则选mysql_native_password这样兼容性问题最少。CREATE USER parking_applocalhost IDENTIFIED WITH mysql_native_password BY parking123; GRANT SELECT, INSERT, UPDATE, DELETE ON parking2.* TO parking_applocalhost; FLUSH PRIVILEGES;生产环境尽量不要让四个权限都开放但课程设计里这套授权已经够清晰。你只需要把 JdbcUtil 里的 USER 和 PASSWORD 改成这个账号就可以避开很多 MySQL 8 的权限坑。注意别把 root 密码写到代码里提交到公开仓库这是个很差的习惯。4. 用 Swing 搭出可用的界面登录、主窗体、车辆进出与收费逻辑4.1 登录窗口验证账号后打开主窗体密码不参与 SQL 拼接Swing 登录窗口一般是一个 JFrame里面放两个 JTextField、一个 JPasswordField 和三个按钮。关键不是布局多好看而是校验逻辑放对位置用户名和密码先做非空校验再查数据库。查数据库必须用 PreparedStatement不能拼字符串。所谓 SQL 注入只需要在输入框里写一个 OR 11如果你用字符串拼接可能不用密码就能登录。private void doLogin() { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空); return; } String sql SELECT id, username, real_name, role FROM t_user WHERE username ? AND password ?; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { int userId rs.getInt(id); String role rs.getString(role); new MainFrame(userId, role).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } } } catch (SQLException e) { e.printStackTrace(); JOptionPane.showMessageDialog(this, 数据库异常 e.getMessage()); } }这里我用的是 Java 7 的 try-with-resources连接、PreparedStatement、ResultSet 都会自动关闭不用手动调JdbcUtil.close。逻辑顺序也很重要先校验空值再查库查到用户后把 userId 和 role 传给主窗体而不是在登录后再查一次。密码这个问题如果后续想认真一点可以在注册时用 SHA-256 加盐登录时对输入做同样哈希再比对。MySQL 里的 password 字段改成 VARCHAR(64) 就够了。课程设计阶段用明文答辩时可能会被老师追问所以最好提前写好加密工具方法。4.2 主窗体布局BorderLayout JTable左边实时车位右边进出记录主窗体是系统的核心界面。我用 BorderLayout 做整体布局顶部放工具栏左边放车位状态面板中间放 JTable 记录列表底部放状态栏。Swing 的布局管理器很多早期很多人喜欢用绝对定位setLayout(null)拖出来的界面换个屏幕就错位。别偷这个懒用 BorderLayout 配合 JScrollPane 最稳。车位面板可以用 GridLayout 排按钮每个按钮代表一个车位绿色是空闲、红色是占用、灰色是维修。下面这段代码只展示刷新车位的核心方法省略窗口初始化的模板代码private void refreshSpaces() { String sql SELECT space_no, status FROM t_parking_space ORDER BY space_no; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { DefaultTableModel spaceModel getSpaceTableModel(); spaceModel.setRowCount(0); while (rs.next()) { String spaceNo rs.getString(space_no); int status rs.getInt(status); // status 0空闲 1占用 2维修 String statusText status 0 ? 空闲 : (status 1 ? 占用 : 维修); spaceModel.addRow(new Object[]{spaceNo, statusText}); } spaceModel.fireTableDataChanged(); } catch (SQLException e) { e.printStackTrace(); } }这里最容易被忽略的一行是spaceModel.fireTableDataChanged()。因为setRowCount(0)和addRow已经改变了数据Swing 的 JTable 依赖 TableModel 的事件通知来重绘界面如果你只是把数组清空再重新 addRow但没有触发通知界面可能不刷新。关于这一点后面避坑章节还会重点说。底部状态栏我习惯放一个“当前在场车辆数”和“空位数”这两个数字每次车辆进出后都要重新查一次。如果为了让数字实时准可以用javax.swing.Timer每 5 秒刷新一次。注意 Timer 的回调也在 EDT 事件线程里所以查询要尽量快别把大报表放进去。4.3 车辆进入与离开一次出场就是一次带事务的计费计算停车场系统最有价值的代码在这里。车辆进入时要判断月卡是否有效、空车位是否存在车辆出场时要算费用、更新记录、释放车位。这几个步骤要么全成功要么全失败必须用事务包起来。本地下一次的教训是用 MySQL Workbench 手动改了一个字段结果界面里的记录状态和车位状态错乱了。下面给出进场和出场两个方法的骨架。进场时插入一条 statusIN 的记录再把车位状态改为 1。出场时计算出费用更新记录的状态和 exit_time再把车位状态改回 0。这里最关键的坑是查询和更新必须用同一个 Connection并且先把自动提交关掉。public void enterCar(String plateNo, String spaceNo, String operator) throws SQLException { Connection conn JdbcUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 检查这个车位是否空闲 String checkSpace SELECT status FROM t_parking_space WHERE space_no ?; try (PreparedStatement ps conn.prepareStatement(checkSpace)) { ps.setString(1, spaceNo); try (ResultSet rs ps.executeQuery()) { if (rs.next() rs.getInt(status) ! 0) { throw new SQLException(车位已被占用或维修中); } } } // 2. 插入停车记录 String insertRecord INSERT INTO t_car_record(plate_no, space_no, enter_time, status, operator) VALUES(?,?,NOW(),IN,?); try (PreparedStatement ps conn.prepareStatement(insertRecord)) { ps.setString(1, plateNo); ps.setString(2, spaceNo); ps.setString(3, operator); ps.executeUpdate(); } // 3. 把车位置为占用 String updateSpace UPDATE t_parking_space SET status 1 WHERE space_no ?; try (PreparedStatement ps conn.prepareStatement(updateSpace)) { ps.setString(1, spaceNo); ps.executeUpdate(); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); JdbcUtil.close(conn, null, null); } }这个方法的逻辑顺序是有讲究的先查车位状态再插记录最后改车位。为什么最后改车位因为一旦插记录成功但车位状态没改成功事务回滚会把插进去的记录也撤掉不会产生一个“幽灵在场车”。如果反过来先占用车位再插记录插记录失败时车位也能回滚效果一样但要记得所有写操作都用同一个连接执行。NOW()取的是 MySQL 服务器时间比 Java 的new Timestamp()更可靠因为你不用考虑多台机器时间不一致的问题。出场逻辑类似但多了一个费用计算。费用计算单独抽一个方法更清晰不要在 DAO 里直接写公式。下面是一个示例private BigDecimal calculateFee(Timestamp enterTime, Timestamp exitTime, FeeRule rule) { if (enterTime null || exitTime null) { return BigDecimal.ZERO; } long minutes (exitTime.getTime() - enterTime.getTime()) / (60 * 1000); if (minutes rule.getFreeMinutes()) { return BigDecimal.ZERO; } long chargeMinutes minutes - rule.getFreeMinutes(); long units (chargeMinutes rule.getUnitMinutes() - 1) / rule.getUnitMinutes(); BigDecimal total rule.getBasePrice() .add(rule.getUnitPrice().multiply(BigDecimal.valueOf(units))); if (total.compareTo(rule.getMaxDailyFee()) 0) { total rule.getMaxDailyFee(); } return total; }这个计算里有两个关键点第一个是“向上取整”。停 31 分钟按 30 分钟计费时不应该只算 1 个单位应该用(chargeMinutes unitMinutes - 1) / unitMinutes保证不足一个单位按一个单位算第二个是金额必须用 BigDecimal禁止用 double。很多人写费用计算时用double乘一下账目里会时不时出现 19.999999 这种数字后面对账会非常麻烦。4.4 按车牌查记录模糊查询与日统计查询是停车场系统被使用最多的功能。收费员要查“某个车牌最近来过几次”管理员要查“今天总收入多少”。车牌查询用LIKE即可但要注意参数化。public ListCarRecord searchByPlate(String plateNo) throws SQLException { String sql SELECT id, plate_no, space_no, enter_time, exit_time, fee, status FROM t_car_record WHERE plate_no LIKE ? ORDER BY enter_time DESC; ListCarRecord records new ArrayList(); try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % plateNo.trim() %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { CarRecord r new CarRecord(); r.setId(rs.getInt(id)); r.setPlateNo(rs.getString(plate_no)); r.setEnterTime(rs.getTimestamp(enter_time)); r.setExitTime(rs.getTimestamp(exit_time)); r.setFee(rs.getBigDecimal(fee)); r.setStatus(rs.getString(status)); records.add(r); } } } return records; }这里的ORDER BY enter_time DESC是查询记录最常用的排序规则最新进的排在前面。LIKE在 MySQL 里默认对大小写不敏感因为表用的是utf8mb4_general_ci所以输入“京A”和“京a”能查出同一批数据对车牌这种场景比较友好。如果你发现输入大小写查不出来检查建表时的 COLLATE 是不是用了_bin结尾的排序规则。日统计用 SQL 直接聚合不要在内存里去 reduceSELECT DATE_FORMAT(enter_time, %Y-%m-%d) AS day, COUNT(*) AS total_count, COALESCE(SUM(fee), 0) AS total_fee FROM t_car_record WHERE enter_time ? AND enter_time ? GROUP BY day ORDER BY day;这段 SQL 配合 JTable 显示就是一张日报表。两个参数分别传入当天 00:00:00 和第二天 00:00:00避免用LIKE %2025-04-01%导致索引失效。查询统计写在 DAO界面只负责把 List 填进 TableModel职责分开。5. 停车场系统常见坑与排查从连接报错到界面卡死五条踩坑记录5.1 报错 Public Key Retrieval is not allowed 或 SSL 连接失败现象运行测试连接时MySQL 8 抛出java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed或者 SSL 相关的SSLHandshakeException。前者通常出现在驱动和 MySQL 首次握手时后者是在 URL 里没有关闭 SSL 时握手超时或证书校验失败。原因MySQL 8.0 默认用户认证插件是caching_sha2_password如果驱动不是合适的版本客户端在缓存中没有公钥时需要向服务端请求公钥但连接参数没允许。SSL 失败则是本地没有配置证书而 MySQL 默认开了 SSL 支持。解决连接 URL 加上allowPublicKeyRetrievaltrueuseSSLfalseserverTimezoneAsia/Shanghai。最简单的是在 JdbcUtil 里统一写好不要每个 dao 都自己去拼 URL。另外也可以把应用账号的认证插件改成mysql_native_password这个我在 3.3 节已经给过建账号的 SQL。提醒一句allowPublicKeyRetrievaltrue在公网连接下会带来中间人风险这个配置只适合本地开发。5.2 点击“查询”后整个窗口变成白屏或拖不动现象点击按钮后按钮像被按住不放窗口整体无响应系统占用率高过一会儿才恢复。严重时直接显示“Not Responding”。原因Swing 的界面事件是在 EDT事件分发线程里执行的你在事件监听器里直接查数据库等于把该画界面的线程拿去阻塞等待数据库返回。数据库慢一秒窗口就卡一秒数据库卡死窗口直接白屏。解决耗时的数据库查询放到 SwingWorker 的doInBackground里查询完成后再回到 EDT 更新界面。这是 Swing 并发处理的标准姿势。searchButton.addActionListener(e - { new SwingWorkerListCarRecord, Void() { Override protected ListCarRecord doInBackground() { try { return carRecordDao.searchByPlate(searchField.getText()); } catch (SQLException ex) { ex.printStackTrace(); return Collections.emptyList(); } } Override protected void done() { try { ListCarRecord data get(); refreshTable(data); } catch (Exception ex) { ex.printStackTrace(); } } }.execute(); });这里doInBackground跑在后台线程done()跑在 EDT 线程所以刷新表格是安全的。不要把数据库连接、查询操作写进done()否则又回到了卡死状态。界面卡死这个问题只要你有过一次血泪经验以后写所有 Swing 事件都会先想一句“这个操作要多久”超过 100 毫秒的都会考虑 SwingWorker。5.3 JTable 不刷新数据库状态已经变了界面还显示旧值现象停车位明明出场后变成空闲但主界面点“刷新”后还是红色或者新增一条记录后 JTable 里看不到。有些同学反复重启程序还是不行最后发现是缓存了旧的 TableModel 引用。原因JTable 不会主动感知数据库变化它只知道自己的 TableModel。常见的错误是在refreshSpaces里spaceModel.setRowCount(0)之后没有调用fireTableDataChanged导致界面拿到的还是旧数据另一个错误是每次刷新都 new 一个 TableModel 并且table.setModel(newModel)但旧引用还挂在别处。解决统一用一个 DefaultTableModel 实例刷新时清空行再addRow最后调用fireTableDataChanged()。如果表格只有几列最简单的方式是直接把模型数据重建model.setRowCount(0); for (Object[] row : rows) { model.addRow(row); } model.fireTableDataChanged();setRowCount(0)本身会触发一个删除事件连续的addRow每次也都会通知界面但最后再补一次fireTableDataChanged不会贵到哪里去却能让整表重绘更彻底。如果用了自定义的 TableModel重写getValueAt后必须在你修改的数据源变化后手动调用fireTableDataChanged。5.4 中文车牌变成乱码现象MySQL 命令行里插入中文正常Java 界面显示也正常但查询出来的中文乱码或者命令行导入表结构后中文注释变成??。原因三处编码不一致。第一处是 MySQL 表字符集不是 utf8mb4第二处是 JDBC URL 少写characterEncodingutf8第三处是 Java 源文件本身被编辑器保存成了 GBK或者javac编译时没指定-encoding UTF-8。解决统一三处编码。建库时就用DEFAULT CHARACTER SET utf8mb4建表语句也写上CHARSETutf8mb4JDBC URL 加characterEncodingutf8Java 源文件用 IDEA 或 VS Code 保存成 UTF-8 格式。如果在命令行导入 SQL执行set names utf8mb4;再 source 文件。编译时加上javac -encoding UTF-8 com/xxx/*.java。这里最容易被坑的是 Windows 控制台默认 GBK你会以为数据存错了其实数据是对的换个客户端看就正常。用 IDEA 的 Database 面板查一遍最直观。5.5 收费金额差一分钱double 不能算钱现象停 1 小时 20 分钟理论上应收 5 元起步价加 1 个单位 2 元共 7 元实际显示 6.999999999。或者累加一天报表时总和差几分钱。原因Java 的double和float是二进制浮点数十进制小数在二进制里无法精确表示连续累加误差会被放大。金额这种数据直接决定了停车场的账目一分钱都不能错。解决数据库里金额字段用DECIMAL(10,2)Java 里用BigDecimal所有计算都走 BigDecimal。我在 4.3 节已经写了示例。还有一点要注意new BigDecimal(0.1)也是不精确的因为它接收的是 double要写new BigDecimal(0.10)用字符串构造。从 ResultSet 里取金额用getBigDecimal不要getDouble后再转换。6. 下一步直接做这两件事换连接池 用 SQL 对账让系统真正可验收6.1 把 DriverManager 换成 HikariCP现在 JdbcUtil 每次查询都从 DriverManager 新建物理连接改成连接池之后连接是复用的界面在多窗口同时查询时不容易慢。Maven 引入com.zaxxer:HikariCP再加一个 MySQL 驱动然后写一个 ConnectionPool 工具类。没有 Maven 就下载 HikariCP 和 slf4j 的 jar 放到 classpath。private static final HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/parking2 ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); config.setUsername(parking_app); config.setPassword(parking123); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }连接池的参数不需要太多MaximumPoolSize 10 对单机桌面应用已经够用ConnectionTimeout 是拿不到连接时的等待上限避免线程无限等。换完连接池后原来的 JdbcUtil.close 方法逻辑不用改因为dataSource.getConnection()返回的 Connection 关闭时实际是归还给池不是真的断开。6.2 用一条 SQL 完成当日收费对账系统验收前我建议你在管理界面做一个“日报”按钮直接把 4.4 节那段日统计 SQL 跑出来。对账时页面 JTable 显示的总金额要和数据库聚合出的结果一致如果不一致优先检查有没有未出场的记录混在已出场里也就是statusOUT但exit_time IS NOT NULL的脏数据。SELECT DATE_FORMAT(enter_time, %Y-%m-%d) AS day, COUNT(*) AS total_count, SUM(CASE WHEN status OUT THEN 1 ELSE 0 END) AS finished_count, COALESCE(SUM(fee), 0) AS total_fee FROM t_car_record WHERE enter_time 2025-01-01 00:00:00 AND enter_time 2025-01-02 00:00:00 GROUP BY day;我自己的习惯是每次改完收费规则先手动插入一条已知时长的记录出场后拿计算结果和手算核对一遍再去做报表。这个习惯救了我很多次至少三次因为起步价和免费时长的边界算错被查出来。如果你不想在答辩现场被老师问倒建议也给自己留一个这样的验证步骤。这个项目做完后你会对 JDBC、事务和 Swing 事件线程有一个整体认知面试时讲“我是怎么用事务保证停车出场时扣费和车位状态一致的”比背一百道 Java 面试题都真实。希望帮到你。本文还有配套的精品资源点击获取
返回列表