
简介这是一套基于Java Swing与MySQL开发的轻量级停车场管理系统实战项目面向Java初学者及GUI数据库综合实践学习者解决小型车场基础业务管理需求如车辆出入场、计费查询、用户信息维护与管理员权限控制等。资源包共80个文件含25个核心Java源码涵盖UI界面、业务逻辑与数据库交互、47个编译后Class字节码、1个建库SQL脚本含charger/park/station/users四张关键表、2个IDE配置文件及运行截图等整体2.11MB结构清晰便于逐模块阅读与调试。已有728人学习下载项目完整呈现了Swing事件驱动编程、JDBC连接池基础应用、MySQL 8.0数据建模与CRUD操作全流程附带可直接运行的GUI界面与典型业务场景截图适合用于课程设计、毕业实训或Java桌面应用入门进阶实践。1. 停车场管理系统为什么非得用 Java Swing MySQL——不是怀旧是稳得住、改得动、查得清你可能在招聘JD里见过“熟悉Swing开发”被列为加分项也可能在GitHub上刷到过一堆标着“Java停车场系统”的冷门仓库点进去发现界面朴素、代码直白、连Maven都没配——但偏偏能跑能增删车位能按车牌查记录能导出Excel。这不是过时技术的残影而是中小型物业、园区、学校后勤这类场景的真实选择不需要高并发压测但要求本地可部署、数据库可直连、逻辑可快速调整、故障能一眼看出在哪一行。Swing不是UI框架里的顶流但它把“窗口—事件—数据库”这条链路压得极短一个JButton点击触发PreparedStatement执行中间没拦截器、没DTO转换、没Spring Boot自动装配的黑匣子MySQL也不是云原生首选但它让管理员用Navicat点几下就能改字段、看慢查询、备份单表——这对运维人力紧张的单位比“微服务拆分”实在得多。本文不讲Spring Cloud怎么搭只带你用最简路径把“车辆入场→分配车位→计费生成→出场结算”这整条业务线用纯JavaSwingMySQL跑通、调稳、查准。适合刚毕业想补全栈闭环的新手也适合需要快速交付定制化模块的老手——毕竟当甲方说“明天要加个‘VIP月租车位锁定’按钮”你打开MainFrame.java加个JCheckBox改两行SQL打包发exe比改三个微服务配置还快。2. 从零建库到Swing主窗体最小可行系统的三步落地2.1 MySQL建库建表按业务实体拆别一上来就搞ER图停车场核心实体就四个车辆car、车位parking_space、停车记录parking_record、管理员admin。别学教科书搞“用户-角色-权限”大而全先让数据能存进去再说。我习惯用命令行建库避免图形界面默认字符集埋坑mysql -u root -p CREATE DATABASE parking_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE parking_system;建表时重点盯三点status字段用TINYINT而非VARCHAR0空闲/1占用/2维修in_time和out_time必须设DEFAULT CURRENT_TIMESTAMPplate_number加UNIQUE索引——这是后续查重和快速定位的命脉-- 车位表物理位置状态类型固定/临时 CREATE TABLE parking_space ( id INT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(10) NOT NULL UNIQUE, -- 如A01、B15 type ENUM(fixed,temporary) DEFAULT fixed, status TINYINT DEFAULT 0, -- 0:空闲 1:占用 2:维修 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 停车记录表关键in_time必须有索引否则查“当前在场车辆”会慢 CREATE TABLE parking_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_number VARCHAR(15) NOT NULL, space_id INT NOT NULL, in_time DATETIME DEFAULT CURRENT_TIMESTAMP, out_time DATETIME NULL, fee DECIMAL(8,2) DEFAULT 0.00, status ENUM(in,out,abnormal) DEFAULT in, INDEX idx_plate (plate_number), INDEX idx_intime (in_time), INDEX idx_status_intime (status, in_time) );提示parking_record表的status字段用ENUM而非INT是为了防止插入非法值如status99Swing层校验再严也挡不住直接SQL注入——数据库层兜底才是真稳。2.2 Swing主窗体骨架用BorderLayout撑起四大功能区Swing不是靠拖控件堆出来的而是靠布局管理器“卡”出来的。ParkingMainFrame继承JFrame用BorderLayout分五块顶部菜单栏JMenuBar、左侧功能树JTree、中部操作面板JPanel、右侧实时车位图JPanel绘图、底部状态栏JLabel。重点不是炫技是让每个区域职责清晰——比如“左侧功能树”只负责跳转不处理数据“中部操作面板”只管输入和按钮不连数据库。这样改需求时加个“月租车续费”功能只动中部面板不影响车位图渲染逻辑。public class ParkingMainFrame extends JFrame { private JTree functionTree; // 左侧功能树 private JPanel centerPanel; // 中部操作区动态切换 private JPanel mapPanel; // 右侧车位图自定义绘制 private JLabel statusLabel; // 底部状态栏 public ParkingMainFrame() { setTitle(停车场管理系统 v1.0); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 1. 顶部菜单栏文件、查询、报表、帮助 setJMenuBar(createMenuBar()); // 2. 左侧功能树用DefaultMutableTreeNode构建层级 functionTree createFunctionTree(); add(new JScrollPane(functionTree), BorderLayout.WEST); // 3. 中部操作面板初始加载入场登记面板 centerPanel new EntryPanel(); // 自定义JPanel add(centerPanel, BorderLayout.CENTER); // 4. 右侧车位图用paintComponent重绘每格代表一个车位 mapPanel new ParkingMapPanel(); add(new JScrollPane(mapPanel), BorderLayout.EAST); // 5. 底部状态栏 statusLabel new JLabel(就绪 | 当前连接: localhost:3306); add(statusLabel, BorderLayout.SOUTH); pack(); setLocationRelativeTo(null); setVisible(true); } private JTree createFunctionTree() { DefaultMutableTreeNode root new DefaultMutableTreeNode(功能导航); DefaultMutableTreeNode entryNode new DefaultMutableTreeNode(车辆入场); DefaultMutableTreeNode exitNode new DefaultMutableTreeNode(车辆出场); DefaultMutableTreeNode queryNode new DefaultMutableTreeNode(记录查询); DefaultMutableTreeNode reportNode new DefaultMutableTreeNode(报表统计); root.add(entryNode); root.add(exitNode); root.add(queryNode); root.add(reportNode); return new JTree(root); } }逻辑说明createFunctionTree()返回的JTree不绑定事件监听——那是EntryPanel和ExitPanel自己的事。Swing的解耦哲学是组件只管“画什么”不问“为什么画”。后续所有业务逻辑都放在独立Panel类里主窗体只做容器和路由。2.3 JDBC连接池HikariCP不是必须但DriverManager绝对要换掉别用Class.forName(com.mysql.cj.jdbc.Driver)DriverManager.getConnection()这种教科书写法。它每次new Connection都走TCP握手10个并发就卡死。HikariCP轻量仅200KB、启动快、监控全且与Swing无冲突。Maven加依赖dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.0.1/version /dependency初始化连接池时关键参数就三个maximumPoolSize5Swing桌面应用根本用不到50连接、connectionTimeout3000超时3秒报错别让用户干等、idleTimeout600000空闲10分钟回收public class DBConnection { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/parking_system?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(5); config.setConnectionTimeout(3000); config.setIdleTimeout(600000); config.setLeakDetectionThreshold(60000); // 60秒未关闭连接告警 dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }参数说明useSSLfalse是本地开发必需MySQL 8.0默认强制SSLSwing连不上会报Public Key Retrieval is not allowedserverTimezoneAsia/Shanghai解决时间戳存取错乱——这是新手踩坑率最高的点没有之一。3. 车辆入场与出场Swing事件驱动下的数据库事务闭环3.1 入场流程从车牌输入到车位分配三步锁住数据一致性入场不是简单INSERT。真实场景要检查车牌是否已存在未出场记录防重复入场、是否有空闲车位、分配后要更新车位状态。这三步必须在一个事务里完成否则出现“记录写了但车位没占”系统就乱套了。Swing按钮监听器里用try-with-resources确保Connection和PreparedStatement自动关闭// EntryPanel.java 中的入场按钮事件 private void onEntryClick() { String plate plateField.getText().trim(); if (plate.isEmpty()) { JOptionPane.showMessageDialog(this, 请输入车牌号); return; } Connection conn null; PreparedStatement ps null; try { conn DBConnection.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查该车牌是否有未出场记录 ps conn.prepareStatement(SELECT id FROM parking_record WHERE plate_number ? AND status in); ps.setString(1, plate); ResultSet rs ps.executeQuery(); if (rs.next()) { JOptionPane.showMessageDialog(this, 车牌 plate 已在场内请勿重复入场); conn.rollback(); return; } // 2. 查找第一个空闲车位 ps conn.prepareStatement(SELECT id, space_no FROM parking_space WHERE status 0 LIMIT 1 FOR UPDATE); rs ps.executeQuery(); if (!rs.next()) { JOptionPane.showMessageDialog(this, 车位已满); conn.rollback(); return; } int spaceId rs.getInt(id); String spaceNo rs.getString(space_no); // 3. 写入停车记录 更新车位状态同一事务 ps conn.prepareStatement(INSERT INTO parking_record (plate_number, space_id, in_time, status) VALUES (?, ?, NOW(), in)); ps.setString(1, plate); ps.setInt(2, spaceId); ps.executeUpdate(); ps conn.prepareStatement(UPDATE parking_space SET status 1 WHERE id ?); ps.setInt(1, spaceId); ps.executeUpdate(); conn.commit(); JOptionPane.showMessageDialog(this, 入场成功车位 spaceNo); statusLabel.setText(入场成功 | 车牌 plate → spaceNo); } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } JOptionPane.showMessageDialog(this, 入场失败 e.getMessage()); } finally { try { if (ps ! null) ps.close(); if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }关键细节SELECT ... FOR UPDATE是核心——它给查到的那条车位记录加行锁防止两个用户同时点“入场”抢同一个空位。没这句高并发下必出“车位分配冲突”。3.2 出场结算时间计算费用生成状态更新一个都不能少出场逻辑比入场更重要算停留时长、查费率规则、更新两条记录记录表车位表、生成小票。MySQL里用TIMESTAMPDIFF算小时数最稳别用JavaDuration.between()——时区和夏令时会让结果飘// ExitPanel.java 中的出场按钮事件 private void onExitClick() { String plate plateField.getText().trim(); if (plate.isEmpty()) { JOptionPane.showMessageDialog(this, 请输入车牌号); return; } Connection conn null; PreparedStatement ps null; try { conn DBConnection.getConnection(); conn.setAutoCommit(false); // 1. 查找未出场记录 ps conn.prepareStatement( SELECT id, space_id, in_time FROM parking_record WHERE plate_number ? AND status in); ps.setString(1, plate); ResultSet rs ps.executeQuery(); if (!rs.next()) { JOptionPane.showMessageDialog(this, 未找到车牌 plate 的入场记录); conn.rollback(); return; } long recordId rs.getLong(id); int spaceId rs.getInt(space_id); Timestamp inTime rs.getTimestamp(in_time); // 2. 计算费用MySQL直接算小时数向上取整避免Java端时区误差 ps conn.prepareStatement( SELECT CEIL(TIMESTAMPDIFF(MINUTE, ?, NOW()) / 60.0) AS hours FROM dual); ps.setTimestamp(1, inTime); rs ps.executeQuery(); rs.next(); int hours rs.getInt(hours); BigDecimal fee new BigDecimal(hours).multiply(new BigDecimal(5.00)); // 5元/小时 // 3. 更新记录表出场时间、费用、状态 车位表释放 ps conn.prepareStatement( UPDATE parking_record SET out_time NOW(), fee ?, status out WHERE id ?); ps.setBigDecimal(1, fee); ps.setLong(2, recordId); ps.executeUpdate(); ps conn.prepareStatement(UPDATE parking_space SET status 0 WHERE id ?); ps.setInt(1, spaceId); ps.executeUpdate(); conn.commit(); JOptionPane.showMessageDialog(this, String.format(出场成功停留%d小时费用%.2f元, hours, fee.doubleValue())); statusLabel.setText(出场成功 | 车牌 plate → 费用 fee); } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } JOptionPane.showMessageDialog(this, 出场失败 e.getMessage()); } finally { // 同上关闭资源 } }血泪经验CEIL(TIMESTAMPDIFF(MINUTE, in_time, NOW()) / 60.0)这句必须写在SQL里。我曾把计算逻辑移到Java端结果某天服务器时区被运维误改成UTC所有计费少算8小时——客户投诉电话打爆。4. 避坑指南SwingMySQL组合里90%的翻车都发生在这五个地方4.1 现象Swing界面卡死、按钮点击无响应原因Swing是单线程模型Event Dispatch Thread所有UI操作和耗时数据库操作都在同一线程。一旦getConnection()或executeQuery()阻塞超过1秒整个界面冻结。解决用SwingWorker把数据库操作扔到后台线程结果通过done()回调更新UI。别用Thread.sleep()模拟那只是掩盖问题。private void searchRecords() { String plate queryField.getText().trim(); new SwingWorkerListParkingRecord, Void() { Override protected ListParkingRecord doInBackground() throws Exception { return DBQuery.searchByPlate(plate); // 耗时查询 } Override protected void done() { try { ListParkingRecord records get(); updateTable(records); // 在EDT线程安全更新JTable } catch (Exception e) { JOptionPane.showMessageDialog(ParkingMainFrame.this, 查询失败 e.getMessage()); } } }.execute(); }4.2 现象MySQL插入中文变问号????原因MySQL服务端、数据库、表、连接URL四层字符集不一致。常见是建库时用了utf8MySQL的utf8实际是utf8mb3不支持emoji而Java连接URL没指定characterEncodingutf8mb4。解决建库时用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci连接URL加?useUnicodetruecharacterEncodingutf8mb4MySQL配置文件my.cnf中[client]和[mysqld]段都设default-character-setutf8mb4。4.3 现象JTable显示数据但无法编辑双击单元格没反应原因JTable默认ModelDefaultTableModel的isCellEditable()返回false且Swing对表格编辑有严格要求必须实现setValueAt()、getColumnClass()返回对应类型String列不能返回Object。解决自定义TableModel重写关键方法class RecordTableModel extends AbstractTableModel { private ListParkingRecord data new ArrayList(); private final String[] columns {ID, 车牌, 车位, 入场时间, 出场时间, 费用, 状态}; Override public boolean isCellEditable(int row, int column) { return false; // 只读表格禁止编辑 } Override public Class? getColumnClass(int columnIndex) { if (columnIndex 0 || columnIndex 3 || columnIndex 4) return Long.class; if (columnIndex 5) return Double.class; return String.class; } }4.4 现象程序退出后MySQL连接未释放报Too many connections原因Swing的setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)会杀进程但HikariCP的连接池线程可能来不及优雅关闭导致连接堆积。解决重写windowClosing事件主动关闭连接池this.addWindowListener(new WindowAdapter() { Override public void windowClosing(WindowEvent e) { try { if (DBConnection.getDataSource() ! null) { DBConnection.getDataSource().close(); // 主动关闭HikariCP } } catch (Exception ex) { ex.printStackTrace(); } System.exit(0); } });4.5 现象Swing Tree节点勾选后无法获取选中状态原因“swing带勾选框的tree”不是原生支持需用JCheckBoxTree或自定义TreeCellRenderer。但多数人直接往DefaultMutableTreeNode塞Boolean却忘了重写toString()——节点显示为空白勾选状态丢失。解决用JCheckBoxTree第三方组件轻量仅一个类或自定义Renderer时确保getTreeCellRendererComponent()里正确设置JCheckBox状态public class CheckBoxTreeCellRenderer extends DefaultTreeCellRenderer { private JCheckBox checkBox new JCheckBox(); Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); if (value instanceof DefaultMutableTreeNode) { Object userObj ((DefaultMutableTreeNode) value).getUserObject(); if (userObj instanceof CheckBoxNode) { CheckBoxNode node (CheckBoxNode) userObj; checkBox.setSelected(node.isSelected()); checkBox.setText(node.getText()); return checkBox; } } return this; } }5. 报表统计与导出用JFreeChart画趋势图用Apache POI导Excel5.1 日报表生成用JFreeChart画“每小时入场车辆数”折线图Swing里嵌图表不用JavaFXJFreeChart成熟稳定、文档全、导出格式多。核心是构造CategoryDataset——把MySQL查出的每小时数据转成DefaultCategoryDatasetprivate JFreeChart createHourlyChart(ListObject[] hourlyData) { DefaultCategoryDataset dataset new DefaultCategoryDataset(); for (Object[] row : hourlyData) { // row[0]hour, row[1]count dataset.addValue((Number) row[1], 车辆数, String.valueOf(row[0])); } JFreeChart chart ChartFactory.createLineChart( 今日每小时入场车辆数, 小时, 数量, dataset, PlotOrientation.VERTICAL, true, true, false ); // 美化字体、网格线、坐标轴 chart.getTitle().setFont(new Font(微软雅黑, Font.BOLD, 14)); XYPlot plot (XYPlot) chart.getPlot(); plot.getRangeAxis().setAutoRangeIncludesZero(false); plot.setDomainGridlinesVisible(true); plot.setRangeGridlinesVisible(true); return chart; }注意hourlyData从MySQL来SQL用GROUP BY HOUR(in_time)聚合别用Java端循环计算——数据库聚合效率高十倍。5.2 Excel导出Apache POI写.xlsx避开xls兼容性雷POI 5.x支持.xlsx.xls已淘汰导出时注意三点日期单元格设CellStyle、数字列对齐右、表头加粗。关键代码private void exportToExcel(ListParkingRecord records, String filename) { try (XSSFWorkbook workbook new XSSFWorkbook(); FileOutputStream fileOut new FileOutputStream(filename)) { XSSFSheet sheet workbook.createSheet(停车记录); XSSFRow headerRow sheet.createRow(0); String[] headers {ID, 车牌, 车位, 入场时间, 出场时间, 费用, 状态}; for (int i 0; i headers.length; i) { XSSFCell cell headerRow.createCell(i); cell.setCellValue(headers[i]); // 表头样式加粗 XSSFCellStyle style workbook.createCellStyle(); XSSFFont font workbook.createFont(); font.setBold(true); style.setFont(font); cell.setCellStyle(style); } // 数据行 for (int i 0; i records.size(); i) { XSSFRow row sheet.createRow(i 1); ParkingRecord r records.get(i); row.createCell(0).setCellValue(r.getId()); row.createCell(1).setCellValue(r.getPlateNumber()); row.createCell(2).setCellValue(r.getSpaceNo()); // 时间列用CellStyle格式化 XSSFCellStyle dateStyle workbook.createCellStyle(); CreationHelper createHelper workbook.getCreationHelper(); dateStyle.setDataFormat(createHelper.createDataFormat().getFormat(yyyy-mm-dd hh:mm:ss)); XSSFCell inCell row.createCell(3); inCell.setCellValue(r.getInTime()); inCell.setCellStyle(dateStyle); row.createCell(4).setCellValue(r.getOutTime() null ? : r.getOutTime().toString()); row.createCell(5).setCellValue(r.getFee().doubleValue()); row.createCell(6).setCellValue(r.getStatus()); } // 自动列宽 for (int i 0; i headers.length; i) { sheet.autoSizeColumn(i); } workbook.write(fileOut); JOptionPane.showMessageDialog(this, 导出成功 filename); } catch (IOException e) { JOptionPane.showMessageDialog(this, 导出失败 e.getMessage()); } }提示XSSFFont和XSSFCellStyle必须用workbook.createXXX()创建不能复用——POI的样式是Workbook级对象跨sheet复用会崩溃。5.3 实战技巧用MySQL视图简化复杂查询Swing里当普通表用报表常要“各类型车位使用率”写JOIN太长易错。建视图把逻辑封装CREATE VIEW space_usage_view AS SELECT ps.type, COUNT(*) as total_spaces, SUM(CASE WHEN ps.status 1 THEN 1 ELSE 0 END) as occupied_count, ROUND(SUM(CASE WHEN ps.status 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as usage_rate FROM parking_space ps GROUP BY ps.type;Swing里查视图和查表一样public static ListSpaceUsage loadSpaceUsage() { ListSpaceUsage list new ArrayList(); String sql SELECT type, total_spaces, occupied_count, usage_rate FROM space_usage_view; try (Connection conn DBConnection.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(new SpaceUsage( rs.getString(type), rs.getInt(total_spaces), rs.getInt(occupied_count), rs.getDouble(usage_rate) )); } } catch (SQLException e) { e.printStackTrace(); } return list; }我的习惯所有报表SQL都先在MySQL Workbench里跑通再粘到Java里。视图名加_view后缀和业务表隔开避免混淆。这招省下30%调试时间——因为错误在数据库层就暴露了不用进Swing断点。希望帮到你。本文还有配套的精品资源点击获取