Java+Swing+MySQL停车场管理系统开发实战:从表设计到避坑指南
简介基于Java Swing与MySQL构建的停车场管理系统完整源码面向Java初学者、课程设计及毕业设计人群解决车辆出入登记、车位分配、用户管理、交易记录等日常停车管理需求。系统采用MVC分层架构前端由Swing呈现登录窗口、车位预订与车辆出入等操作界面后端通过JDBC连接MySQL涵盖预编译语句防SQL注入、线程并发控制、异常处理等工程实践。资源共79个文件核心包括25个java源文件与47个class字节码文件另附MySQL建表SQL脚本、工程配置文件、依赖jar包及预览图片压缩包约2.08MB导入IDE即可运行学习目前已有2351人学习下载。内容上完整呈现车位表、车辆表、用户表、交易表等数据库设计并保留标准项目目录结构便于逐模块阅读源码、理解桌面应用从界面交互到数据持久化的完整链路。既可作为课程作业参考也可在此基础上进行二次扩展是掌握Java GUI编程与数据库整合技术的实用资料。1. 停车场管理系统这套组合为什么值得你亲手写一遍如果你正在找 Java 课程设计源码、刚学完 Java SE 想做第一个能跑的项目或者面试前想补一个“我独立完成过”的完整案例那 JavaSwingMySQL 停车场管理系统基本是最合适的练手对象。它不像电商系统那样涉及分布式、消息队列也不像管理系统那样堆一大堆 CRUD 页面而是把 Java 基础语法、面向对象编程思想、Swing 组件用法、MySQL 增删改查这四块恰好串在一起难度曲线平缓做完之后能讲清楚的东西又多。常见做法是用 Swing 做桌面客户端MySQL 存车位、车辆和收费记录中间通过 JDBC 连接。整套代码通常几百行到一千多行适合课程设计也适合作为 Java 基础面试的“项目经历”素材。你不需要会 Spring、MyBatis、Maven 这些企业级框架只需把 JDK 和 MySQL 装好就能跑起来。本文用一套可落地的实现方案带你走通全流程从表设计到界面编码再到常见报错排查每一项都给具体代码和参数而不是停留在“大概思路”。2. 先设计数据库再写界面停车场系统的表结构和关键 SQL很多新手拿到这个题目第一件事就是写界面结果写到一半发现车位状态、收费计算、历史记录这些数据不知道存哪里、怎么取。我这里建议的顺序是先做数据库设计把表结构定下来再用数据表反推界面该怎么切。因为 Swing 界面说到底就是“查数据—展示—改数据—再刷新”表结构稳了界面代码就是按图索骥。2.1 五张核心表从车位到收费记录的完整链路停车场管理系统最简可运行版本至少需要五张表车位表、车辆信息表、入场记录表、收费规则表、用户表。不需要做会员充值、月卡绑定这些扩展就先不做把核心链路走通。车位表的核心是“车位编号”和“状态”。状态字段我建议用 int0 表示空闲、1 表示占用比用字符串“空闲”“占用”更省空间也方便在 SQL 里做统计。车辆信息表用于登记固定车辆比如车主姓名、车牌、手机号它和入场记录是一对多的关系。入场记录表是整个系统的核心。每次车辆进场插一条记录记录车牌、入场时间、入场时的车位编号出场时更新这条记录的出场时间和应收金额。这里需要注意“当前是否在停”这个状态推荐用 leave_time 是否为空来判断而不是单独搞一个 status 字段因为出院时间为空就表示还没走逻辑上更直观。收费规则表要支持按小时计费比如首小时 5 元、之后每小时 3 元、24 小时内封顶 30 元封顶和阶梯价是面试时会被追问的点。用户表就是登录用的账号密码存 MD5 或 SHA-256 哈希值即可课程设计直接存明文也能跑但我不建议这么做养成好习惯对后续职业发展有帮助。建表语句可以直接在 Navicat for MySQL 或 MySQL Workbench 里执行CREATE DATABASE IF NOT EXISTS parking_system DEFAULT CHARSET utf8mb4; USE parking_system; CREATE TABLE parking_space ( space_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 车位ID, space_no VARCHAR(10) NOT NULL UNIQUE COMMENT 车位编号如A001, status INT NOT NULL DEFAULT 0 COMMENT 0空闲 1占用, remark VARCHAR(100) DEFAULT NULL ) ENGINEInnoDB; CREATE TABLE vehicle ( vehicle_id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL UNIQUE COMMENT 车牌号, owner_name VARCHAR(50), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE parking_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, space_id INT NOT NULL COMMENT 车位ID, enter_time DATETIME NOT NULL COMMENT 入场时间, leave_time DATETIME DEFAULT NULL COMMENT 出院时间NULL表示在场内, amount DECIMAL(10,2) DEFAULT NULL COMMENT 应收金额, CONSTRAINT fk_record_space FOREIGN KEY (space_id) REFERENCES parking_space(space_id) ) ENGINEInnoDB; CREATE TABLE charge_rule ( rule_id INT PRIMARY KEY AUTO_INCREMENT, first_hour_fee DECIMAL(10,2) NOT NULL DEFAULT 5.00, per_hour_fee DECIMAL(10,2) NOT NULL DEFAULT 3.00, daily_cap DECIMAL(10,2) NOT NULL DEFAULT 30.00 ) ENGINEInnoDB; CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT SHA-256哈希值, real_name VARCHAR(50) ) ENGINEInnoDB;这套 DDL 有几点值得关注。第一数据库字符集统一用 utf8mb4不然存车牌号里的中文没问题但以后如果要存特殊符号就会报错这是 MySQL 安装配置教程里反复强调的兼容性设置。第二外键只加了 parking_record 到 parking_space 这一条vehicle 表不强制与 record 关联因为临时车辆进场时不会先登记在 vehicle 表里入场记录直接记车牌号更灵活这个设计取舍面试时可以主动讲。第三金额字段用 DECIMAL(10,2) 而不是 float这个选择背后是血泪经验——用 float 算金额0.1 加 0.2 会出现精度误差虽然界面看起来差异不大但做毕业设计答辩演示时一旦被问到“为什么金额是 9.999999”就翻车了。2.2 初始化数据和核心查询语句表建好之后要注意车位表和规则表需要初始化数据不然程序跑起来一片空白。常见做法是写一段 SQL 批量插入车位比如一层 50 个车位编号从 A001 到 A050INSERT INTO parking_space(space_no) SELECT CONCAT(A, LPAD(n, 3, 0)) FROM ( SELECT 1 n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 -- 实际写的时候可以用存储过程或者程序循环插入这里只示意 ) t; INSERT INTO charge_rule(first_hour_fee, per_hour_fee, daily_cap) VALUES (5.00, 3.00, 30.00); INSERT INTO sys_user(username, password, real_name) VALUES (admin, SHA2(123456, 256), 系统管理员);车位插入这种用 UNION 写死的方式只适合初始化数据量小的场景。实际课程设计中常见做法是写一个 InsertInitData 类在程序启动时检测车位表是否为空为空则用 for 循环执行 INSERT这样不用手动在 SQL 里拼接 50 条语句。程序里生成编号用 String.format(A%03d, i) 即可和 SQL 里 LPAD 的效果一致。密码用 SHA2 函数是 MySQL 8 的内置能力注意 MySQL 5.6 及以下版本写法不同如果你装的还是 5.7建议改成 SHA1 或者直接在 Java 里用 MessageDigest 算好再入库避免版本兼容的坑。核心查询主要就三类查询停车场当前剩余车位、按车牌查车辆在场信息、查历史收费记录。剩余车位就是 status0 的数量SELECT COUNT(*) AS free_count FROM parking_space WHERE status 0;车辆进场时先按 plate_no 查是否已在场内防止同一辆车重复入场SELECT record_id FROM parking_record WHERE plate_no ? AND leave_time IS NULL;出院结算时要根据入场时间算停车时长再按收费规则算金额。金额计算在 SQL 里写会很绕常见做法是把规则查出来在 Java 里写一个 Calculator 方法逻辑更清晰调试也方便。这个我放在最后一章详解。3. Swing 界面开发从登录到主窗体的完整实现路径数据库设计好了界面就能照着表结构来写。Swing 的核心套路是 JFrame 或 JDialog 承载窗体JPanel 布局组件JTable 展示表格数据事件监听器响应按钮点击。千万不能把所有代码塞进 main 方法或一个类里一旦超过三百行调试的时候你会怀疑人生。3.1 项目包结构和数据库连接基类先定包结构这是给答辩和面试讲项目时最容易出彩的地方。常见做法按三层划分com.parking.view 放界面类com.parking.dao 放数据库访问类com.parking.model 放实体类com.parking.util 放工具类。这就体现了一个理念——界面和逻辑分离几乎相当于手写了一个简化版 MVC。数据库连接是第一个要写的基础类。这里有个常见的翻车点直接在界面类里写 DriverManager.getConnection每点一次按钮就创建一个连接用完不关程序跑一会儿就报连接数耗尽。正确做法是写一个统一的 DBUtil 类用静态方法获取连接所有 DAO 都从它这里拿MySQL 8 之后不需要再手动加载 Class.forName(com.mysql.jdbc.Driver)这个类在 8.x 里已经改名了驱动路径不一致会抛 ClassNotFoundException。package com.parking.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 注意 MySQL 8 的 url 要加 serverTimezone否则会报时区错误 private static final String URL jdbc:mysql://localhost:3306/parking_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你的数据库密码; static { try { // 驱动路径在 8.x 是 com.mysql.cj.jdbc.Driver不是旧版的 com.mysql.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException 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(); } } } } }这段代码里最值得留意的是 URL 参数。characterEncodingutf8 保证中文不乱码serverTimezone 是 MySQL 8 的强制要求如果不写连接时直接报时区异常。有人用 useSSLfalse 关掉 SSL 告警这也可以但根本原因是 MySQL 8 默认开启 SSL 而客户端证书链没配置如果你本地开发不涉及网络安全要求加 useSSLfalse 确实是消除告警的常见做法。驱动路径从 com.mysql.jdbc.Driver 改成 com.mysql.cj.jdbc.Driver是很多人按旧教程抄代码时遇到的第一个坑报错信息是 ClassNotFoundException: com.mysql.jdbc.Driver光记住这个报错面试时就能刷掉一批照着旧贴吧教程写代码的人。close 方法用了可变参数和 AutoCloseable 接口这样调用时写 DBUtil.close(rs, stmt, conn) 一行就能把三个资源都关掉避免 finally 块里写一堆 try-catch。3.2 登录界面Swing 组件布局和事件监听的最小范例登录是大多数管理系统界面的起点它的代码量不大但涉及 Swing 的典型套路JLabel 放文字、JTextField 放输入、JPasswordField 放密码、JButton 触发动作。布局用绝对定位还是 GridBagLayout 都行课程设计追求简单直观常用做法是 null 布局加 setBounds 硬定坐标代码短但对于不同分辨率屏幕的适配能力差。我建议用 GridLayout 或 BorderLayout 配合 JPanel 嵌套代码量多一点但答辩时能讲出布局管理器的概念。package com.parking.view; import javax.swing.*; import java.awt.*; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import com.parking.util.DBUtil; public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { setTitle(停车场管理系统 - 登录); setSize(400, 250); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 窗口居中 JPanel panel new JPanel(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); gbc.insets new Insets(10, 10, 10, 10); gbc.gridx 0; gbc.gridy 0; panel.add(new JLabel(用户名), gbc); gbc.gridx 1; usernameField new JTextField(15); panel.add(usernameField, gbc); gbc.gridx 0; gbc.gridy 1; panel.add(new JLabel(密码), gbc); gbc.gridx 1; passwordField new JPasswordField(15); panel.add(passwordField, gbc); JButton loginBtn new JButton(登 录); gbc.gridx 0; gbc.gridy 2; gbc.gridwidth 2; panel.add(loginBtn, gbc); loginBtn.addActionListener(e - doLogin()); setContentPane(panel); setVisible(true); } private void doLogin() { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()).trim(); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空, 提示, JOptionPane.WARNING_MESSAGE); return; } String sql SELECT real_name FROM sys_user WHERE username ? AND password SHA2(?, 256); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { String realName rs.getString(real_name); JOptionPane.showMessageDialog(this, 欢迎 realName); new MainFrame().setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误, 错误, JOptionPane.ERROR_MESSAGE); } } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, 数据库连接失败 ex.getMessage(), 错误, JOptionPane.ERROR_MESSAGE); } } public static void main(String[] args) { // Swing 界面必须要在事件分发线程中创建这是规范写法 SwingUtilities.invokeLater(() - new LoginFrame()); } }这段代码里有几个关键点。try-with-resources 写法比传统 finally 块简洁得多它会自动关闭 Connection 和 PreparedStatement前提是这两个类都实现了 AutoCloseable 接口ResultSet 也实现了。登录验证使用了 PreparedStatement 的参数占位符而不是字符串拼接 SQL这是面试必问的 SQL 注入防范点你可以在答辩时说用 ? 占位符由驱动预编译用户输入的内容只会被当作参数处理而不会改变 SQL 结构。密码用 SHA2 哈希再做比对而不是直接存明文虽然课程设计很多源码直接明文存但用哈希值是 MySQL 安装配置之后应该有的安全意识。SwingUtilities.invokeLater 是 Swing 的线程规则所有 UI 组件必须在事件分发线程EDT创建和更新main 方法直接 new LoginFrame 在多数情况下能跑但遇到复杂的刷新逻辑时会偶发界面卡死或不刷新这在后面避坑章节我再展开。这里先记住入口统一用 SwingUtilities.invokeLater 包一层。3.3 主窗体与车位状态面板用 JTable 展示实时数据登录成功之后进入主窗体它负责展示整层车位的状态。常见做法有几种有人用 JButton 网格模拟车位平面图按钮变红表示占用、变绿表示空闲这种做法的优点是直观缺点是车位多了之后界面代码冗长。另一类做法用 JTable 展示车位列表再加“入场登记”“出院结算”“刷新”三个按钮实现成本低。课程设计的时间有限我建议用 JTable 方案。主窗体要解决的核心问题是“每次操作后怎么刷新表格”。你的 DAO 查询结果是一个 List 要把这个集合转成 TableModel 的数据一个常见做法是继承 AbstractTableModel 写一个自定义模型这样在数据变化后调用 fireTableDataChanged() 就能通知表格重绘不用每次重建 JTable 对象。package com.parking.view; import javax.swing.*; import javax.swing.table.AbstractTableModel; import java.awt.*; import java.util.List; import com.parking.dao.SpaceDao; import com.parking.model.ParkingSpace; public class SpaceTableModel extends AbstractTableModel { private final String[] columns {车位编号, 状态, 当前车辆}; private ListParkingSpace data; public SpaceTableModel(ListParkingSpace data) { this.data data; } Override public int getRowCount() { return data null ? 0 : data.size(); } Override public int getColumnCount() { return columns.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { ParkingSpace space data.get(rowIndex); switch (columnIndex) { case 0: return space.getSpaceNo(); case 1: return space.getStatus() 0 ? 空闲 : 占用; case 2: // 关联字段由 DAO 查询时联表填充 return space.getCurrentPlateNo(); default: return ; } } Override public String getColumnName(int column) { return columns[column]; } public void setData(ListParkingSpace newData) { this.data newData; fireTableDataChanged(); } }这里有一个设计上的巧思第三列“当前车辆”不在 parking_space 表里而是通过连表查询从 parking_record 里拿最新记录的车牌。这意味着你的 DAO 查询 SQL 要写成 LEFT JOIN而不是只查单表。这也是在实际开发中经常遇到的问题界面需要展示的数据往往来自多张表一次查出来填充到模型里比一行一行查然后拼装要高效得多也符合“避免 N1 查询”的常识。public ListParkingSpace listAllWithVehicle() { String sql SELECT s.space_id, s.space_no, s.status, (SELECT r.plate_no FROM parking_record r WHERE r.space_id s.space_id AND r.leave_time IS NULL ORDER BY r.enter_time DESC LIMIT 1) AS current_plate FROM parking_space s ORDER BY s.space_no; // try-with-resources 执行查询并逐行封装成 ParkingSpace 对象 }关联子查询在这里比 JOIN 更直观因为它明确表达了“取当前在场且最近入场的一条记录”这个语义。子查询里 LIMIT 1 是保险起见因为同一车位理论上不应出现两条在场记录但数据被手工改过之后就会出现脏数据加上 LIMIT 1 只是不让程序崩掉真正的修复还是要靠数据库约束和业务逻辑双重把关。主窗体本身布局不复杂顶部放操作按钮中间放表格底部放状态栏显示剩余车位数。按钮的入场登记和出院结算都通过 JDialog 弹窗完成这里不再展开每个窗体的完整代码核心模式是JDialog 里放若干 JTextField 和 JButton点确定后调用 DAO 方法成功则刷新主表格和状态栏。4. Swing 与 MySQL 联调避坑指南五个高频故障的排查实录Swing MySQL 的坑基本集中在三个层面环境配置、JDBC 参数、Swing 线程模型。我把课程设计过程中最常见的五类问题按现象、原因、解决三段式列出来你照着排查能省下一大半调试时间。4.1 ClassNotFoundException: com.mysql.jdbc.Driver现象程序一运行就抛 ClassNotFoundException提示找不到驱动类。原因你用了 MySQL 8 的驱动 jar 包但代码里写的是 5.x 的驱动路径。MySQL 8 的驱动类名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver旧类只是在新版本里保留了一个别名在某些驱动版本中甚至被移除了。另外一个常见原因是驱动 jar 没有正确放进构建路径Eclipse 里很多人把 jar 拖进项目目录就以为完事了其实还要右键 Build Path 添加。解决检查你的 mysql-connector-java jar 包版本。如果是 8.x代码里用 Class.forName(com.mysql.cj.jdbc.Driver)如果是 5.x就用旧类名。如果你用的是 Maven 项目pom 里加依赖后记得刷新项目。这个坑的隐蔽之处在于网上大量教程还是 2018 年以前写的照着抄旧代码配新驱动必踩。提示JDBC 4.0 之后驱动可以自动注册理论上不写 Class.forName 也能连。但为了兼容老环境和让报错信息更容易定位保留这行代码是常见稳妥做法。4.2 Communications link failure 或连不上 localhost现象界面一启动就报 Communications link failure 或者 Cant connect to MySQL server on localhost (10061)。原因这个报错要看两个方向。一是 MySQL 服务根本没有启动Windows 上查看右下角任务栏或服务管理器里 MySQL 是否在运行Linux 上常见做法是 systemctl status mysqld 或者 service mysql status。二是 JDBC URL 里端口号写错了默认是 3306但有些人是 3307、3308 多实例安装。解决先用命令行客户端测试mysql -u root -p 是否可以连上。命令行能连而程序不能连问题出在 URL 参数命令行也不能连先去启动服务。还有一个隐蔽因素MySQL 8 默认只监听 localhost 还是允许远程连接取决于安装时的配置如果程序连接时把 URL 写成 127.0.0.1 没问题写成 localhost 报错而改成 127.0.0.1 就正常说明本机 hosts 解析异常这种情况在 Windows 上偶有发生。4.3 中文乱码表数据正常界面显示问号现象数据库用 Navicat 看是中文程序里一查出来就变成 ?? 或者乱码写入的中文在数据库里也是乱码。原因字符集在多个环节传输任何一个环节不统一都会乱码。数据库表是 utf8mb4、JDBC URL 没加 characterEncodingutf8、IDE 源文件本身是 GBK 编码这三个环节只要有一个不一致中文就不正常。课程设计里常见的是 IDE 默认编码和 URL 编码不一致比如 Eclipse 默认 GBK代码里写死的中文串没问题但从数据库读出的 UTF-8 内容显示就乱。解决统一字符集。数据库建库时用 utf8mb4JDBC URL 加上 useUnicodetruecharacterEncodingutf8IDE 工程编码设为 UTF-8。注意一个细节如果数据库和 URL 都对了只有 IDE 源文件是 GBK那么 SQL 语句里的中文参数照样乱。Eclipse 在项目属性 - Resource - Text file encoding 里改成 UTF-8IDEA 在 Settings - File Encodings 里把 Global Encoding、Project Encoding、Default encoding for properties 都设成 UTF-8。检查顺序先库里再 URL最后 IDE。4.4 MySQL 8 的 SSL 连接报错和时区报错现象连接时报 SSL connection error: protocol version mismatch 或者 The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8 默认开启 SSL客户端没有正确证书就会告警或报错时区问题是因为驱动连接时拿不到系统时区而 MySQL 服务器的时区设置可能是系统默认值。解决URL 里加 useSSLfalseserverTimezoneAsia/Shanghai。这两个参数在 MySQL 8 的基本就是固定搭配本地开发关闭 SSL 不会带来实际问题因为 MySQL 客户端和服务器在同一台机器上不经过网络传输敏感数据。如果项目部署到服务器上就应该反过来认真配置 SSL 证书而不是图省事关掉这是个安全意识问题面试聊到这里是加分项。4.5 Swing 界面卡死点按钮后窗体无响应现象点击“查询”或“刷新”按钮后整个窗口卡住拖动不了也关闭不了过几秒甚至几分钟后恢复期间 CPU 占用极高。原因这是 Swing 线程模型的经典问题。按钮事件回调默认运行在事件分发线程EDT上如果你的回调里执行了耗时操作比如查询几百条数据、网络延迟EDT 被阻塞整个窗口就不响应了。MySQL 数据量小时几乎感觉不到一旦 parking_record 表里测试数据超过几万条每次查询都要好几秒卡顿就非常明显。解决耗时的数据库操作放到后台线程。常见做法是用 SwingWorker 或者直接 new Thread(() - {...}).start()查询完成后通过 SwingUtilities.invokeLater 把结果回填到界面。不要在工作线程里调用 component.setText 或 table.setModel这些操作必须回到 EDT。简单示例如下new SwingWorkerListParkingSpace, Void() { Override protected ListParkingSpace doInBackground() throws Exception { // 这里是后台线程可以执行数据库查询 return spaceDao.listAllWithVehicle(); } Override protected void done() { // 这里回到 EDT 线程可以放心更新界面 try { ListParkingSpace list get(); tableModel.setData(list); } catch (Exception e) { JOptionPane.showMessageDialog(MainFrame.this, 数据加载失败, 错误, JOptionPane.ERROR_MESSAGE); } } }.execute();SwingWorker 比直接 new Thread 更好的地方在于它内置了后台线程执行和 EDT 回调的切换机制doInBackground 在后台跑done 自动切回 EDT代码结构也比匿名线程清晰。这个模式值得熟记任何 Swing 项目里遇到耗时操作都用它界面就不会再被吐槽“卡得像死机”。5. 出院结算与收费计算一个可以讲十分钟的亮点模块大多数课设项目都把出场逻辑做成“输入车牌算个金额收钱走人”但这个模块恰恰是答辩提问的高发区收费规则能不能灵活改跨天停车怎么算首小时和后续小时怎么衔接如果你能在这一块做出层次整套项目的完成度评价会高一大截。5.1 分段收费计算方法把规则从 SQL 中抽出来收费规则存在 charge_rule 表里但计算逻辑放在 Java 层。原因有两个一是规则可能频繁调整改数据库就行不用改代码二是分段计算逻辑在 SQL 里写 CASE WHEN 很痛苦效率和可读性都差。常见算法是停车不足一小时按一小时计费超过首小时后按整小时累加但每日封顶金额优先于累加金额。public BigDecimal calcAmount(LocalDateTime enterTime, LocalDateTime leaveTime, ChargeRule rule) { long minutes Duration.between(enterTime, leaveTime).toMinutes(); if (minutes 0) { throw new IllegalArgumentException(出院时间必须晚于入场时间); } // 不足 60 分钟按 1 小时算 long hours (minutes 59) / 60; BigDecimal amount; if (hours 1) { amount rule.getFirstHourFee(); } else { // 首小时费用 后续小时数 * 每小时费用 long extraHours hours - 1; amount rule.getFirstHourFee() .add(rule.getPerHourFee().multiply(BigDecimal.valueOf(extraHours))); } // 超出每日封顶则按封顶收这里按自然日 24 小时算 if (amount.compareTo(rule.getDailyCap()) 0) { amount rule.getDailyCap(); } return amount.setScale(2, RoundingMode.HALF_UP); }这套计算逻辑的细节值得展开分钟向上取整是很多开源代码喜欢用 Math.ceil(minutes / 60.0) 但边界条件处理不严谨我这里用 (minutes 59) / 60 是纯整数运算避免浮点误差。BigDecimal 的 compareTo 负责封顶判断和 equals 不同compareTo 只看数值不考虑小数位。入场和出院时间段跨多个自然日呢最简单的做法是每天按封顶计算再补最后一日的不足 24 小时费用这个逻辑可以作为扩展点写在文档里答辩时主动提出来“当前版本按单次停车不超过 24 小时假设若跨天需要拆分计算”比被问到再说要体面得多。这个方法放在一个独立的 ChargeCalculator 类里出入场按钮调用它便于单独写测试或 main 方法验证边界。常见实验数据入场 10:00、出场 11:30按首小时 5 元、后续每小时 3 元结果为 5 3 8 元入场 10:00、出场 22:30时长 12.5 小时向上取整 13 小时金额为 5 12 * 3 41 元再与 30 元封顶比较后收 30 元。这两组数据应该作为单元测试用例确保每次改动后不回归。5.2 出院操作的状态流转事务保证数据一致出院不是只算个金额就完事它涉及三张表的状态变更parking_record 要更新 leave_time 和 amountparking_space 要把 status 改回 0。两步操作必须在一个数据库事务里完成否则会出现“车走了但车位还是占用”的状态错乱。public void checkout(String plateNo) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查当前在场记录 String selectSql SELECT record_id, enter_time, space_id FROM parking_record WHERE plate_no ? AND leave_time IS NULL; // 2. 计算金额 // 3. UPDATE parking_record SET leave_time NOW(), amount ? WHERE record_id ? // 4. UPDATE parking_space SET status 0 WHERE space_id ? conn.commit(); } catch (Exception e) { conn.rollback(); // 回滚到事务开始前 throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }很多课程设计源码出场逻辑就两句 UPDATE 不带事务表面看没问题一旦在改记录和改车位之间程序抛异常数据就永久错乱。你把这个事务写进项目面试被问到“怎么保证数据一致性”时就有真实案例可讲事务保证了多步写操作的原子性配合 setAutoCommit(false) 与 rollback 机制要么全部成功要么全部不成功。这里还需要注意事务期间不要让界面假死所以 checkout 方法建议也通过 SwingWorker 调用。注意连接池如果用的是 HikariCP 或 Druid事务写法不变但连接关闭是归还池子而不是真正断开。课程设计直接用 DriverManager 也不用纠结这个区别理解事务语义即可。5.3 验收清单跑通这套项目的验证步骤做完之后不要直接交按下面清单过一遍确保答辩演示时不出安全事故。第一步把 MySQL 服务停掉再启动登录程序能正常弹窗提示数据库连接失败而不是直接崩溃说明异常处理到位。第二步同一辆车连续入场两次系统应该弹出“该车已在场内”的提示这一步在 DAO 查询环节校验界面层也要拦截一次双保险。第三步入场和出院后分别查车位状态确认车位占用和释放正确。第四步用之前列的两组数据验算收费金额把结果和手算的对比确认收费逻辑符合规则。至于扩展方向常见的有三条路。给 vehicle 表加月卡到期时间在出院计算时优先按包月处理把 Swing 界面换成 JavaFX 或 Web 前端业务逻辑和 DAO 层基本不用动这也是向面试官证明你理解分层价值的好素材对 parking_record 表加索引优化大数据量下的查询速度课程设计阶段可以在 10 万条数据下测一测加索引前后的响应时间差异写进论文里也是亮点。我自己的习惯是在出院结算按钮旁边放一个“打测试数据”的隐藏入口快速插入一批模拟车辆记录这样演示时不用反复手动录入也方便测大数据量下的界面流畅度。这些细节不会写进课程设计报告但调试时能省下大量时间。整套写下来你会发现与其说这是课设不如说你提前走了一遍真实项目里“设计、编码、联调、踩坑、优化”的最小闭环。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →