简介:面向Java课程设计和Swing入门者的停车场管理系统完整项目包。系统实现了用户登录注册、浏览车辆信息、添加车辆入场/出场信息、删除与查询车辆信息等基础管理功能,完整覆盖从MySQL表设计到JDBC读写、再到Swing窗体交互的典型开发链路。压缩包共49个文件,以12个Java源文件、31个编译生成的class文件为主,另有project/prefs等Eclipse工程配置、SQL建表脚本、运行截图和JAR依赖包,解压后可直接导入开发环境并运行,便于对照源码理解各模块逻辑。资源整体约1.96MB,轻量紧凑;目前已有467人参与学习下载。适合需要完成Java课程设计、复习Swing桌面应用开发或梳理MySQL-Java连接流程的读者,借助完整代码与数据库脚本可快速跑通项目,并在此基础上扩展功能。
1. Java+Swing+MySQL停车场管理系统:课程设计之外,它到底能走到哪一步
搜索"基于Java+Swing+MySQL停车场管理系统",跳出来的结果大多是课程设计选题和毕业设计成品。但如果你真打算把这套三件套从demo做成能顶岗用的桌面系统,要解决的问题比想象中多得多:Swing 的事件模型怎么和 MySQL 事务配合,计费逻辑怎么处理跨天和月卡边界,上千条出入场记录怎么查询才不卡界面。这篇不铺大而全的架构图,就沿着停车场进出场这条主线,把建表、连库、界面、计费、排错每个环节的代码、参数和坑一次说透。适合正在做课程设计收尾的人,也适合刚入行想拿桌面项目练 JDBC 和 MySQL 基本功的开发者。
2. 技术选型为什么是这三件套:Swing 的边界与 MySQL 的担当
2.1 Swing 不是古董:桌面管理系统的真实适用场景
先解决一个选型问题:做桌面管理系统,为什么还选 Swing,而不是 JavaFX 或 Electron?Swing 是 JDK 自带的 UI 组件库,不需要额外装运行时,打包成可执行 jar 后双击就能跑。停车场岗亭的场景很典型:一台普通 Windows 电脑、一个显示器、操作员用鼠标键盘点按钮输车牌。这种轻交互、单机部署(连一台内网 MySQL)的形态,Swing 完全撑得住。JavaFX 界面更现代,但需要额外配模块,在无网环境和老机器上反而容易出幺蛾子。Electron 则是另一个极端,一个停车管理系统打包出来几百 MB,还要带 Node 运行时,杀鸡用牛刀。
但 Swing 的边界必须心里有数:它不适合做复杂的可视化大屏,不适合触摸屏手势操作,界面美观度要做自定义 LookAndFeel 才能勉强跟上审美。常见做法是,把 Swing 当"业务表单壳"来用——所有复杂计算全部放 Service 层,界面层只负责接收输入、调用方法、把结果填进 JTable。这样后面哪天真要换 JavaFX 或者 Web 管理后台,业务逻辑一行都不用动。
另一个选型理由是生态兼容性。MySQL 是中小型系统里占比最高的开源数据库,教程多、问题好搜,配合 JDBC 原生驱动不需要引入 MyBatis 或 Hibernate 这类重型 ORM。对一个表结构固定、查询语句有限的管理系统,原生 JDBC 的可控性反而更高。排错时直接看到 SQL 语句本身,不用猜框架底层替你做了什么。
2.2 计费规则先行:先画费率配置,再写界面
实际做这套系统最容易翻车的不是 Swing 布局,而是计费规则。停车场收费模式一般有四种,很多场景是组合出现的:
| 模式 | 规则示例 | 边界风险 |
|---|---|---|
| 按时收费 | 首小时5元,之后每小时2元,不足1小时按1小时 | 跨天怎么算、每日封顶 |
| 按次收费 | 进场一次固定10元 | 超时重进怎么判定 |
| 月卡/年卡 | 套餐时段内不限次 | 到期当天出场怎么处理 |
| 免费时段 | 前30分钟免费 | 免费时段跨边界怎么算 |
这四种模式叠加时,边界问题成倍出现:入场在免费时段内、出场在免费时段外,费用要按超出的分钟数算;入场是当天 23:50、出场是第二天 00:20,要不要按两天收费;月卡用户月卡当天到期,当天出场要不要收临时费。这些问题不提前定义清楚,写代码时每遇到一个边界就临时改逻辑,改到后面没人敢动。
我的顺序是:先建费率配置表,把每条规则存成数据行;再写一个独立的计费服务类,输入入场时间、出场时间、车辆类型,输出应收金额。Swing 界面只做三件事——收集参数、调用服务、显示结果。最后测试时只需要针对计费服务类写几个边界用例,不需要反复点界面,省下来的时间比想象中多。
2.3 表结构设计:六张核心表撑起进出场与计费全流程
数据库设计上,按六张核心表来落,覆盖登录、车辆类型、费率、车位、出入场记录,每张表都加 create_time 和 update_time 两个审计字段,后面排查数据非常有用。
- t_user:登录账号,存用户名、密码(BCrypt 哈希)、角色。
- t_vehicle_type:车辆类型字典,如临时车、月卡车、免费车。
- t_rate_config:费率配置,关联车辆类型,存计费方式、单价、免费分钟数、每日封顶金额。
- t_parking_slot:车位表,存车位编号、所在区域、当前状态,空闲/占用状态冗余存储是为了进场时快速分配。
- t_entry_record:入场记录,存车牌号、车辆类型、入场时间、车位编号、操作员。
- t_exit_record:出场结算记录,关联入场记录,存出场时间、应收金额、实收金额、支付方式。
索引设计上,入场记录的入场时间字段建普通索引,车牌号建普通索引。出场结算要按车牌倒查最近一条在场记录,报表要按时间范围聚合,这两个索引能避免全表扫描。车位表的状态字段也值得建索引,停车场规模大时,几百上千个车位的查询效率差距非常明显。
这六张表的关联关系不复杂,核心是出场记录通过入场记录 ID 关联到入场记录,再通过车辆类型关联到费率配置。真正要注意的是金额字段一律用 DECIMAL(10,2),不要用 FLOAT 或 DOUBLE——计费精确到分,浮点类型在累加时会出现 0.1 加 0.2 不等于 0.3 的问题,这在 Java 的 double 里同样存在,属于踩过的坑。
3. 数据库与工程骨架:从建库到登录窗跑通的最小路径
3.1 建库建表 SQL:字段、索引与初始化数据一次到位
先给建库建表脚本,按 MySQL 5.7 及以上版本的语法写。字符集统一用 utf8mb4,排序规则用 utf8mb4_general_ci,避免后面中文乱码。装好 MySQL 之后,直接用命令行或 Navicat 执行这段 SQL 就能把库建起来,不需要额外安装其他组件。
CREATE DATABASE IF NOT EXISTS parking_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE parking_system; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 'BCrypt哈希后的密文', role VARCHAR(20) NOT NULL DEFAULT 'OPERATOR', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB; INSERT INTO t_user (username, password, role) VALUES ('admin', '$2a$10$...哈希值...', 'ADMIN');这里的字符集参数是关键。utf8mb4 是 MySQL 里真正完整支持中文(包括生僻字和 emoji)的字符集,utf8 在 MySQL 5.7 里实际是 utf8mb3,遇到特殊字符会报 "Incorrect string value" 错误。密码字段建议存 BCrypt 哈希而不是 MD5,MD5 用彩虹表很快就能撞出来,属于 Java 面试里常被追问的安全点,实际项目中至少要用 BCrypt 或 PBKDF2。
CREATE TABLE t_vehicle_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(20) NOT NULL UNIQUE, description VARCHAR(100) ) ENGINE=InnoDB; INSERT INTO t_vehicle_type (type_name, description) VALUES ('临时车', '按次或按时收费'), ('月卡车', '按套餐时段不限次'), ('免费车', '内部车辆免费通行');CREATE TABLE t_rate_config ( id INT PRIMARY KEY AUTO_INCREMENT, vehicle_type_id INT NOT NULL, billing_mode VARCHAR(20) NOT NULL COMMENT 'HOURLY/MONTHLY/FREE', unit_price DECIMAL(10,2) NOT NULL DEFAULT 0, free_minutes INT NOT NULL DEFAULT 0, daily_cap DECIMAL(10,2) DEFAULT NULL, FOREIGN KEY (vehicle_type_id) REFERENCES t_vehicle_type(id) ) ENGINE=InnoDB;费率配置表是计费逻辑的核心表。HOURLY 模式配合 unit_price 表示每小时费用,free_minutes 表示免费分钟数,daily_cap 是每日封顶金额,NULL 表示不封顶。MONTHLY 模式不参与单次计费,只需要在程序里判断车辆是否在套餐有效期内。把规则数据化之后,改费率不用改代码,改一条数据库记录就行,这是这套设计里最值钱的地方。
CREATE TABLE t_parking_slot ( id INT PRIMARY KEY AUTO_INCREMENT, slot_no VARCHAR(20) NOT NULL UNIQUE, area VARCHAR(50) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1占用' ) ENGINE=InnoDB; CREATE TABLE t_entry_record ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL, vehicle_type_id INT NOT NULL, slot_id INT NOT NULL, entry_time DATETIME NOT NULL, operator VARCHAR(50), status TINYINT NOT NULL DEFAULT 0 COMMENT '0在场 1已出场', INDEX idx_plate (plate_no), INDEX idx_entry_time (entry_time), FOREIGN KEY (vehicle_type_id) REFERENCES t_vehicle_type(id) ) ENGINE=InnoDB; CREATE TABLE t_exit_record ( id INT PRIMARY KEY AUTO_INCREMENT, entry_record_id INT NOT NULL, exit_time DATETIME NOT NULL, amount DECIMAL(10,2) NOT NULL DEFAULT 0, actual_amount DECIMAL(10,2) NOT NULL DEFAULT 0, pay_method VARCHAR(20) DEFAULT 'CASH', operator VARCHAR(50), FOREIGN KEY (entry_record_id) REFERENCES t_entry_record(id) ) ENGINE=InnoDB;索引点再强调一下:plate_no 和 entry_time 单独建索引,出场时要按车牌倒查在场记录,报表要按时间范围统计,没有这两个索引,数据量到几千条时查询耗时就会明显上升。entry_record 的 status 字段也建议建索引,查在场车辆数时走索引比全表扫快很多。这里建的是普通索引不是唯一索引,因为同一辆车可以多次进场,只是同一时刻只能有一条 status=0 的记录,这个约束放在业务层判断。
3.2 JDBC 连接封装:为什么这里不需要 MyBatis
对这类表结构固定、查询语句有限的桌面系统,用原生 JDBC 加一个轻量工具类就够了,不引 MyBatis。理由很直接:连接串、账号、密码全部放在一个配置文件里,工具类负责加载驱动、建立连接、关闭资源,业务类只关心 SQL 和参数,代码量比框架少而且更好排查。
public class JdbcUtil { private static String url; private static String username; private static String password; static { try (InputStream in = JdbcUtil.class.getResourceAsStream("/db.properties")) { Properties props = new Properties(); props.load(in); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); Class.forName("com.mysql.cj.jdbc.Driver"); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }静态代码块里加载配置和驱动,类第一次被引用时只执行一次。配置文件放在 resources 目录下,JdbcUtil.class.getResourceAsStream("/db.properties") 从 classpath 根目录读取,打包成 jar 后也能正常工作。这里依赖 JDK 自带的 java.util.Properties,不需要引入第三方依赖。要提醒一点,运行这套程序需要先配好 Java 环境变量,确保 java 命令能直接用,不然连驱动类都加载不了,报 ClassNotFoundException。
连接串建议这样写,把参数显式声明,避免踩默认值的坑:
jdbc.url=jdbc:mysql://127.0.0.1:3306/parking_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true逐个说明这些参数:characterEncoding=utf8 保证中文字符在连接层不被转乱;serverTimezone=Asia/Shanghai 解决 MySQL 驱动 8.x 在 JDK 8 以上时出现的时区报错 "The server time zone value";useSSL=false 是因为本地开发环境不需要 SSL 加密,MySQL 5.7 默认开了 SSL 选项,不关会给出 warning 甚至连接失败;allowPublicKeyRetrieval=true 是配合 MySQL 8 的 caching_sha2_password 认证插件,连接时用明文密码传输需要获取服务器公钥。关连接的顺序也有讲究,ResultSet、Statement、Connection 要逆序关闭,而且必须放在 finally 里。工具类的 close 方法把空判断也做了,业务层一行 JdbcUtil.close(conn, stmt, rs) 收尾,不会因为某次查询异常而漏关连接。连接泄漏是最隐蔽的性能杀手,后面避坑章节单独讲。
3.3 登录窗口到主窗体:卡片布局让界面切换不再重启
登录窗体是 Swing 程序最直观的一层。常见做法是:先弹登录窗体,验证通过后 dispose 掉登录窗体,再 new 主窗体。但这类系统不止一个窗口,主窗体内部还有面板切换,用 JFrame 加 JPanel 卡片布局来管理,避免多个 JFrame 互相叠置的混乱。
public class MainFrame extends JFrame { private CardLayout cardLayout; private JPanel cardPanel; public MainFrame() { setTitle("停车场管理系统"); setSize(900, 600); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); cardLayout = new CardLayout(); cardPanel = new JPanel(cardLayout); cardPanel.add(new EntryPanel(), "ENTRY"); cardPanel.add(new ExitPanel(), "EXIT"); cardPanel.add(new ReportPanel(), "REPORT"); add(cardPanel, BorderLayout.CENTER); addMenuBar(); } private void addMenuBar() { JMenuBar bar = new JMenuBar(); JMenu menu = new JMenu("业务操作"); JMenuItem entryItem = new JMenuItem("进场登记"); JMenuItem exitItem = new JMenuItem("出场结算"); entryItem.addActionListener(e -> cardLayout.show(cardPanel, "ENTRY")); exitItem.addActionListener(e -> cardLayout.show(cardPanel, "EXIT")); menu.add(entryItem); menu.add(exitItem); bar.add(menu); setJMenuBar(bar); } }CardLayout 的好处是面板实例常驻内存,切换不重建、不丢状态。比如进场面板里正在录的车牌信息,切到出场面板再切回来,还在那里。如果用多个 JFrame 的方式,这些状态就要靠全局变量或者重新查库找回,体验差很多。登录验证里有一点要提醒:不要把 SQL 拼成字符串,正确写法是用 PreparedStatement 占位符,防止 SQL 注入。Java 基础里 PreparedStatement 为什么能防注入属于高频面试题,实际项目里这也是底线要求,用户输入的任何字符串只能当参数,不能当 SQL 片段。
4. 核心业务落地:进场、出场计费与统计报表
4.1 进场登记:车牌录入与车位联动
进场登记的流程不复杂:输入车牌,选择车辆类型,系统自动分配空闲车位,写入入场记录,把车位状态改成占用。推荐的做法是先查空闲车位列表,下拉让操作员选,同时车位状态在选中的瞬间更新,防止两个岗亭同时分配同一个车位。
public int entryParking(String plateNo, int vehicleTypeId, int slotId) { String checkSql = "SELECT id FROM t_entry_record WHERE plate_no = ? AND status = 0"; String entrySql = "INSERT INTO t_entry_record (plate_no, vehicle_type_id, slot_id, entry_time, status) VALUES (?, ?, ?, NOW(), 0)"; String updateSlotSql = "UPDATE t_parking_slot SET status = 1 WHERE id = ? AND status = 0"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement checkStmt = conn.prepareStatement(checkSql)) { checkStmt.setString(1, plateNo); ResultSet rs = checkStmt.executeQuery(); if (rs.next()) { return -1; // 该车牌已在场内,不能重复进场 } conn.setAutoCommit(false); try (PreparedStatement entryStmt = conn.prepareStatement(entrySql, Statement.RETURN_GENERATED_KEYS); PreparedStatement updateStmt = conn.prepareStatement(updateSlotSql)) { entryStmt.setString(1, plateNo); entryStmt.setInt(2, vehicleTypeId); entryStmt.setInt(3, slotId); entryStmt.executeUpdate(); updateStmt.setInt(1, slotId); int rows = updateStmt.executeUpdate(); if (rows == 0) { conn.rollback(); return -2; // 车位已被占用 } conn.commit(); return 1; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } } catch (SQLException e) { throw new RuntimeException("进场登记失败", e); } }这里核心是把两件事放进同一个事务:写入入场记录、更新车位状态。如果先插入记录再更新车位,更新失败时记录已经写进去了,车却没进场,数据就对不上。MySQL 事务处理在 InnoDB 下默认是自动提交的,这里手动 setAutoCommit(false),两个 SQL 一起成功或一起回滚。updateSlotSql 里带了 status = 0 条件,作用是乐观锁,两个岗亭同时操作同一个车位时,只有第一个能更新成功,第二个影响行数为 0,程序返回"车位已被占用"。这是用一次条件更新代替显式行锁的常见做法,并发量不大的桌面系统足够用,比 SELECT ... FOR UPDATE 轻量很多。
4.2 出场结算:计时计费与事务边界
出场结算是整个系统最核心的方法。流程是:输入车牌或入场单号,查出最近一条 status=0 的入场记录,根据车辆类型取费率,计算应收金额,写入出场记录,把入场记录 status 改成 1,释放车位。
public BigDecimal calculateFee(int vehicleTypeId, LocalDateTime entryTime, LocalDateTime exitTime) { // 实际应从费率表读取配置,这里以参数说明为主 // mode=HOURLY, unitPrice=5.00, freeMinutes=30, dailyCap=50.00 long minutes = Duration.between(entryTime, exitTime).toMinutes(); long billableMinutes = minutes - freeMinutes; // 先抵扣免费时段 if (billableMinutes <= 0) { return BigDecimal.ZERO; } BigDecimal hours = BigDecimal.valueOf((billableMinutes + 59) / 60); // 不足1小时按1小时 BigDecimal amount = hours.multiply(unitPrice); if (dailyCap != null && amount.compareTo(dailyCap) > 0) { amount = dailyCap; } return amount.setScale(2, RoundingMode.HALF_UP); }计费规则里两个参数最容易错。第一,不足一小时按一小时,用 (billableMinutes + 59) / 60 整数除法实现向上取整,如果用 double 除再强转会有精度隐患。第二,涉及金额的乘法,BigDecimal 一定要用 String 构造参数,new BigDecimal(5.0) 会把二进制浮点误差带进来,new BigDecimal("5.00") 才是干净的十进制表达。金额保留两位用 setScale,舍入方式用 HALF_UP,这是财务结算的标准做法。
出场结算同样要包事务:插入出场记录、更新入场记录状态、更新车位状态,三个操作一个事务。别小看这个事务边界,如果插入出场记录成功但更新入场状态失败,下一次同一个车牌再出场时会查出两条在场记录,系统直接乱掉。结算面板里把实收金额和找零做到同一行,方便现金收费场景,支付方式下拉里放现金、微信、支付宝。对于月卡车辆,结算前先查套餐有效期,在有效期内直接生成金额为 0 的出场记录,不走按时计费。
4.3 统计报表:用 SQL 聚合代替内存循环
报表模块最容易写歪。很多初学者会把数据全部查出来,然后在 Java 里用 for 循环分组求和,数据量一上千就明显卡顿。正确做法是让 MySQL 先聚合,Swing 只负责显示。
SELECT DATE_FORMAT(entry_time, '%Y-%m-%d') AS day, COUNT(*) AS entry_count, SUM(CASE WHEN t2.amount IS NOT NULL THEN 1 ELSE 0 END) AS exit_count, COALESCE(SUM(t2.amount), 0) AS total_amount FROM t_entry_record t1 LEFT JOIN t_exit_record t2 ON t1.id = t2.entry_record_id WHERE t1.entry_time >= ? AND t1.entry_time < ? GROUP BY DATE_FORMAT(entry_time, '%Y-%m-%d') ORDER BY day;这段 SQL 把日报表的统计一次算完。LEFT JOIN 保证凌晨进场还没出场的记录也能出现在计数里,COALESCE 把 NULL 金额补成 0。GROUP BY 按天聚合,SUM(CASE WHEN ...) 是 MySQL 里做条件计数的常用写法,比先查出来再 if 判断快一个数量级。报表查询结果用 JTable 填充,DefaultTableModel 直接 addRow,上千行渲染没问题。但要注意别在事件分发线程里做这种时间跨度大的聚合查询,卡界面问题后面避坑章会展开。
日报之外,车位利用率和月卡到期提醒也可以加到这个模块里。车位利用率就是 SUM(status) 除以 COUNT(*) 乘 100,一条 SQL 就能出结果。月卡到期提醒查 t_vehicle_type 关联的月卡车辆入场记录,筛选出场时间在七天内的,JTable 里标黄显示。这些功能代码量不大,但能让这个系统从"能交作业"变成"能真用"。
5. Swing + MySQL 避坑指南:四条血泪经验
5.1 界面假死:点击查询后窗口拖不动
现象:点击查询按钮后整个窗口白屏转圈,鼠标拖不动,只能强制结束进程。
原因:把耗时 SQL 直接写在 ActionListener 里,查询期间占住了事件分发线程(EDT)。Swing 是单线程模型,所有界面刷新都要经过 EDT,查询一执行,整个窗口就假死。
解决:用 SwingWorker 把耗时操作放到后台线程,完成后回到 EDT 更新 JTable。
SwingWorker<List<ReportRow>, Void> worker = new SwingWorker<>() { @Override protected List<ReportRow> doInBackground() { return reportService.queryDailyReport(start, end); } @Override protected void done() { try { List<ReportRow> rows = get(); // 在这里更新JTable的TableModel } catch (Exception e) { JOptionPane.showMessageDialog(MainFrame.this, "查询失败: " + e.getMessage()); } } }; worker.execute();SwingWorker 的两个核心方法要分清:doInBackground 在后台线程执行,done 在 EDT 上回调。get() 会拿到后台线程的返回值,如果后台抛异常,get 会把它包在 ExecutionException 里重新抛出来,所以必须 try-catch 后弹提示,不能让异常无声吞掉。这是 Java 多线程与 Swing 集成的标准解法,也是 Java 基础里线程协作的一个典型落地场景。
5.2 中文乱码:车牌显示成问号
现象:界面输入中文车牌后,往 MySQL 写入再查出来,显示成 ????。
原因:两层问题叠加。第一层是数据库连接串没带 characterEncoding=utf8,第二层是建库时默认字符集不是 utf8mb4。
解决:连接串补上 useUnicode=true&characterEncoding=utf8,建库语句显式指定 DEFAULT CHARACTER SET utf8mb4。另外 MySQL 5.7 的 utf8 实际是 utf8mb3,只支持 BMP 字符,如果车牌或备注里要支持生僻字,必须用 utf8mb4。改完这两个地方,重启程序再测试一次进出场全流程。还有一个隐蔽点:Windows 下的命令行客户端如果代码页不是 65001,手工往表里插中文也会乱,但这个问题只影响调试,不影响 Java 程序读写。
5.3 跨天计费:金额算成负数被客户投诉
现象:23:50 进场的车,00:20 出场,理论上停了 30 分钟,收费系统却报出负数金额或者直接报错。
原因:计费方法只算了进场当天的时长,用 出场时间减去当天 0 点 再减去 入场时间到当天 0 点的差值,日期切换后差值变成负值。
解决:计算费用前统一用 LocalDateTime 做差值,Duration.between 计算的是绝对时长,不会为负。如果业务要求跨天按两天收费,那就先判断 entryTime.toLocalDate() 和 exitTime.toLocalDate() 是否不同,再按每天分别调用计费规则。我在计费服务里加了一个 isCrossDay 方法,返回值直接绑到出场界面的一个红字标签上,操作员一眼就能看到这单是跨天单。这个坑属于业务规则问题而不是技术问题,写代码前和停车场管理员确认清楚最省事。
5.4 连接泄漏:系统跑几天后报 Too many connections
现象:程序正常运行两三天后,突然所有查询都报 "Too many connections",重启后恢复,过几天又犯。
原因:某处查询异常时没有调用 JdbcUtil.close,Connection 一直占用不释放。MySQL 默认 max_connections 是 151,桌面系统同时打开的连接数不多,但泄漏的连接日积月累,最终把上限撑爆。
解决:一是强制所有 JDBC 操作都写在 try-with-resources 里,代码块结束自动关闭;二是做一次全项目排查,搜所有 DriverManager.getConnection 的调用点,确认每条路径都有 finally 或 try-with-resources 收口。try-with-resources 是 Java 7 引入的语法,也是面试里常问到的点,实际项目里它就是连接泄漏最有效的解药。业务量再大一点,可以用 HikariCP 连接池兜底,但小型桌面系统原生 JDBC 够用,重点是关闭要严谨,不要依赖什么玄学重启。
6. 从能跑到能用:初始化数据校验与备份的最后一公里
系统跑通当天只是开始,真正让它扛住真实停车场的一天,还差几件小事。
第一件是初始化数据校验。每次启动程序时,检查 t_rate_config 表里是不是有可用的费率配置,车位表里有没有车位,一张表一条 SQL 的事。没有配置就弹警告框,提示管理员先去系统设置里补数据,而不是让操作员进场时才发现费率是零。第二件是操作日志落盘。每次进出场、结算、修改费率,都往一个本地日志文件里追加一行记录,用最简单的 java.nio.file.Files.write 带 APPEND 参数就行。数据库里只能看到最终状态,日志能告诉你这个状态是怎么一步步变成的,出问题排查时是后悔药级别的存在。第三件是 MySQL 数据备份。桌面系统一般没有专职 DBA,我在主窗体加了一个"备份数据"按钮,内部调用 mysqldump 命令把整个 parking_system 库导出到带时间戳的 SQL 文件,操作员每天下班点一下,比任何云备份都可靠。
还有一个小习惯帮我省了很多事:把 JTable 的列宽、窗体的位置大小这些界面参数也存到数据库配置表里,而不是写死在代码里。操作员觉得列太窄,直接在界面上拉一下,下次启动还是这个宽度,不用重新编译。我自己做这套系统最大的教训就是一开始把精力全花在界面美化上,实际上真正撑住场面的是干净的 SQL、明确的事务边界、严谨的资源关闭这三件事。界面丑一点,操作员一周就习惯了;数据乱了,一个月都补不回来。希望帮到你。
本文还有配套的精品资源,点击获取