
简介这是一份面向Java学习者的GUI综合项目资源实现了智慧公交管理系统的完整业务闭环覆盖车辆信息、员工信息、线路站点、排班方案等核心模块并严格区分管理员、调度员、员工三类角色权限。项目基于Java Swing图形界面开发采用MySQL 8.0作为数据库百分百可运行适合课程设计、毕业设计或企业实训场景。压缩包共2个文件包含完整的Java工程源码压缩包与busmanage.sql数据库脚本脚本内置员工表、登录表、排班表等七张数据表导入MySQL后即可与代码直连运行整体大小10.72MB。目前已有1136人学习下载。通过该资源读者可直接运行Main包中的Login.java登录系统对照源码与SQL脚本理解图形界面与数据库交互的完整流程还能在此基础上扩展新功能是快速上手Java GUI数据库开发的实用范例。1. 这套能跑到答辩现场的Java GUI公交管理系统拆开给你看做Java课程设计最怕的不是写不出代码而是从网上下载一个项目后导入IDEA全是报错数据库连不上登录按钮点了像没装一样。这次要拆的这套智慧公交管理系统是纯Java Swing写的桌面客户端配合MySQL 8.0里的busmanage库把车辆信息、员工信息、线路信息、站点信息、排班方案管理全接在了一起还按管理员、调度员、员工三种角色做了独立登录入口。它适合用来交数据库大作业也适合那些想把java基础、JDBC、Swing串起来的开发者当作练手项目慢慢啃。这个系统的技术栈是Java 8 Swing JDBC没有MyBatis、Hibernate这些框架所以整个调用链路反而很透明你能清楚看到Connection怎么打开、PreparedStatement怎么绑定参数、ResultSet怎么遍历更新JTable。代码量不大但该有的逻辑都在。更重要的是它不是一个只有空壳界面的演示项目登录用的用户名密码分别放在三张登录表里不同角色登录后看到的菜单不一样这就是一个完整业务系统该有的骨架而不是一个练习控件的demo。下面从数据库设计、登录验证、业务模块、排班关联、部署排错这条线逐一拆解每一步都给出可以直接抄的代码。2. 数据库先行busmanage的表结构与JDBC连接参数2.1 七张表怎么通过主外键串起来拿到压缩包后先不要急着打开Java文件先把busmanage.sql看一遍。这个项目一共涉及员工表、登录表、排班表、站点表、线路表、车辆表、线路-站点表数据库名是busmanage运行环境是MySQL 8.0。数据表之间不是孤立的员工表通过emp_id和登录表关联线路表和站点表通过route_station多对多关联排班表又同时引用线路、车辆、员工。这种设计对应的是公交公司“定线、定站、定车、定人”的现实模型。下面用一张表把核心字段串起来方便后面写SQL时对照列名。表名核心字段作用employeeemp_id, emp_no, emp_name, role员工主数据loginlogin_id, emp_id, username, password统一登录账号vehicleid, plate_no, type, seats, status车辆信息lineid, line_name, start_name, end_name线路信息stationid, station_name站点信息route_stationid, line_id, station_id, station_order线路途经站点顺序scheduleid, line_id, vehicle_id, emp_id, run_date排班方案注意员工表里的role字段存放的是角色名称例如管理员、调度员、员工。而登录表为什么单独拆出来因为密码字段不适合和业务字段混在一起拆开之后修改密码时不需要动员工主记录两表通过外键emp_id关联。2.2 用SQL快速复现核心表结构一般大作业都会直接给busmanage.sql在MySQL Workbench 8.0 CE里导入就行。但如果你想自己重建或者遇到导入报错最好能手写核心建表语句。建表顺序很重要因为外键约束要求先有主表才能建子表。-- 员工表基础数据的源头 CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 员工ID, emp_no VARCHAR(20) UNIQUE COMMENT 工号, emp_name VARCHAR(50) COMMENT 姓名, role VARCHAR(10) COMMENT 角色管理员/调度员/员工, phone VARCHAR(20) COMMENT 电话, hire_date DATE COMMENT 入职日期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工信息表; -- 登录表与员工表一对一 CREATE TABLE login ( login_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT, username VARCHAR(50) UNIQUE, password VARCHAR(100), FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT登录账号表;这段SQL里ENGINEInnoDB和高版本MySQL默认一致但显式写出来能让人一眼知道这是支持事务和外键的引擎。CHARSET设置为utf8mb4是为了让中文站点名、线路名不出现乱码。如果你用的是老版本的sql文件里面可能只写了utf8在MySQL 8.0里也能跑但utf8mb4能兼容更多特殊符号。建表顺序建议是employee、login、vehicle、line、station、route_station、schedule。route_station依赖line和stationschedule依赖employee和vehicle顺序反了会报外键错误。2.3 JDBC连接参数的坑时区、驱动类名、编码这个项目要求使用mysql-connector-java-8.0.29.jar对应MySQL 8.0配合JDK 1.8。许多老教程里还在用com.mysql.jdbc.Driver这在MySQL 8.0里会直接ClassNotFoundException。8.x驱动必须写成com.mysql.cj.jdbc.Driver。另一个高频报错是The server time zone value解决办法是在连接串末尾加serverTimezoneAsia/Shanghai。Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/busmanage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; String user root; String password 123456; Connection conn DriverManager.getConnection(url, user, password);这段代码的重点不在Class.forName而在连接串参数。useUnicodetrue和characterEncodingutf8共同保证中文写入数据库不乱码之前有人往站点表里插“人民广场”变成“”就是漏了characterEncoding。serverTimezoneAsia/Shanghai解决数据库时区差八小时的问题useSSLfalse是为了避免测试环境SSL握手警告。2.4 初始化数据与账号分离这个项目有一个比较特殊的设计管理员、调度员、员工三者的用户名密码分别存放在三张不同的登录表里。从业务上看这是为了简化不同角色的权限判断登录时先选身份再查对应的表。但从数据库设计角度来说这个方案冗余度偏高更常规的做法是只建一张login表加一个role字段再用视图或Java代码区分权限。既然项目原版这么设计我们在使用时就按原版来不要擅自把三张表并成一张否则登录模块也要跟着重写。初始化数据时注意不要删掉admin账号。有些同学在测试修改密码功能时把管理员密码改掉随后忘记新密码只能回去改数据库。常见做法是准备一个init.sql专门存放测试账号和演示数据例如管理员账号admin/admin123调度员dispatcher/123456员工staff/123456方便重置。3. 登录验证与角色权限Login.java里到底做了什么3.1 登录按钮的事件监听与数据库查询打开Main包里的Login.java会发现登录逻辑其实很直接用户选择角色类型输入账号密码点击登录按钮后在ActionListener中根据角色拼接不同的表名然后执行SELECT查询。这也是整个项目最核心的入口后面的主窗体靠登录结果决定能打开哪些业务面板。String role (String) cmbRole.getSelectedItem(); String table ; if (管理员.equals(role)) { table admin_login; } else if (调度员.equals(role)) { table dispatcher_login; } else { table staff_login; } String sql SELECT * FROM table WHERE username? AND password?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { JOptionPane.showMessageDialog(this, 登录成功); new MainFrame(role, username).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); }这段代码里用了PreparedStatement而不是Statement拼接字符串这是值得写到简历上的一个点它能防止SQL注入。因为用户输入的内容被当作参数绑定而不是直接拼进SQL。不过这里的表名仍然是动态拼接的而表名不能用占位符所以需要确保role选项是固定的几个值千万不要直接用用户输入的表名。3.2 不同角色看到不同菜单的实现登录成功后进入MainFrame主窗体一般用JDesktopPane放多个JInternalFrame或者用CardLayout切换面板。角色不同界面菜单项不同最简单的方法是在初始化菜单前查一次权限Map。下面的代码展示了一种常见的做法把菜单权限集中到一个Map里方便维护。MapString, ListString menuPermission new HashMap(); menuPermission.put(管理员, Arrays.asList(车辆管理, 员工管理, 线路管理, 站点管理, 排班管理, 修改密码)); menuPermission.put(调度员, Arrays.asList(线路管理, 站点管理, 排班管理, 修改密码)); menuPermission.put(员工, Arrays.asList(我的排班, 修改密码)); JMenu menu new JMenu(业务功能); for (String item : menuPermission.get(role)) { JMenuItem menuItem new JMenuItem(item); menu.add(menuItem); }这种设计比一长串if判断清晰得多。以后新增角色时只需要往Map里加一组数据不用改动主窗体的布局代码。在真实项目中菜单权限常常来自数据库这里可以直接把Map替换成查询结果再把权限码存到表里效果一样。3.3 修改密码模块里的PreparedStatement用法登录模块之外修改密码也是必须的。不少同学第一次写修改密码时只做了UPDATE没有校验原密码或者校验完原密码后忘了用新密码覆盖。正确流程是先用原密码查一次登录表查到了再执行UPDATE否则提示原密码错误。建议把登录表名也按角色分开这样复用前面登录时选表的逻辑。String checkSql SELECT * FROM table WHERE username? AND password?; PreparedStatement psCheck conn.prepareStatement(checkSql); psCheck.setString(1, username); psCheck.setString(2, oldPwd); ResultSet rs psCheck.executeQuery(); if (rs.next()) { String updateSql UPDATE table SET password? WHERE username?; PreparedStatement psUpdate conn.prepareStatement(updateSql); psUpdate.setString(1, newPwd); psUpdate.setString(2, username); int rows psUpdate.executeUpdate(); if (rows 0) { JOptionPane.showMessageDialog(this, 密码修改成功请重新登录); } } else { JOptionPane.showMessageDialog(this, 原密码错误); }注意这里没有把修改后的会话强制退出只是弹窗提示。更严谨的写法是修改成功后调用dispose()关掉当前窗口再跳回登录界面否则用户还留在主界面操作会产生“账号已改密码但旧会话仍有效”的误解。4. 核心业务界面车辆、员工、线路、站点的增删改查4.1 用JTable展示数据库结果集的通用方法车辆管理、员工管理、线路管理、站点管理这几个窗口表面看是不同的业务代码结构几乎一样无非是一个查询SQL配上几个文本框然后展示在JTable里。初学者往往把每个窗口都写一遍完全独立的代码实际上可以抽一个公共方法把ResultSet转换为TableModel。public void loadData(JTable table, String sql, Object... params) { DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { ResultSetMetaData meta rs.getMetaData(); int cols meta.getColumnCount(); while (rs.next()) { Object[] row new Object[cols]; for (int i 1; i cols; i) { row[i - 1] rs.getObject(i); } model.addRow(row); } } } catch (SQLException e) { e.printStackTrace(); } }这段代码用ResultSetMetaData动态读取列数好处是SQL的列变化后Java层不用改。但JTable的列头需要提前设置否则显示不出来你可以在初始化时用model.setColumnIdentifiers(new Object[]{ID,车牌号,车型,核载,状态})。注意try-with-resources写法可以自动关闭Connection、PreparedStatement、ResultSet避免连接泄漏。4.2 车辆管理的字段校验与参数绑定车辆信息管理是业务模块里最标准的CRUD。界面通常包含车牌号、车型、核载人数、购置日期、状态这几个字段。新增时关键是把文本框里的值通过setString绑定到SQL语句中而不是直接拼接。INSERT INTO vehicle (plate_no, type, seats, status, buy_date) VALUES (?, ?, ?, ?, ?);这里的plate_no必须唯一录入前最好先查重复。seats可以用JFormattedTextField限制只能输入数字也可以在点击保存时用正则校验。buy_date字段最麻烦JTextField里手输日期经常会格式错误建议用第三方日期组件或JSpinner。我一般会在保存前先做一次format校验格式不合法直接弹窗而不是等数据库报错。4.3 删除时的外键保护与外键约束车辆表会被排班表引用因此直接执行DELETE很可能会因为外键约束失败而报错。这时候有几种处理方案大作业里最常用的是逻辑删除也就是给vehicle表加一个status状态字段把status从“运营”改为“停运”本质上不删数据。下面表格列出了物理删除、逻辑删除、级联删除的差异方便你根据自己项目情况选择。删除方式实现方式优点风险物理删除DELETE FROM vehicle WHERE id?SQL简单外键报错不可恢复逻辑删除UPDATE vehicle SET status0 WHERE id?保留数据查询时都要加status过滤级联删除ON DELETE CASCADE自动清理子表误删无法恢复删除按钮的代码应该捕获SQLIntegrityConstraintViolationException然后弹窗提示“该车辆已被排班引用无法删除”而不是让用户看到一长串英文异常堆栈。4.4 员工管理中的角色分配与登录账号联动员工管理模块稍微复杂一点因为新增员工时除了插入employee表还要往对应的登录表里插入一条账号。如果插入员工表成功但登录表失败就会产生一个无法登录的孤儿账号。这里需要手动控制事务Java里用setAutoCommit(false)开启事务成功提交失败回滚。conn.setAutoCommit(false); try { PreparedStatement ps1 conn.prepareStatement( INSERT INTO employee(emp_no, emp_name, role, phone, hire_date) VALUES(?,?,?,?,?), Statement.RETURN_GENERATED_KEYS); ps1.setString(1, empNo); ps1.setString(2, empName); ps1.setString(3, role); ps1.setString(4, phone); ps1.setDate(5, Date.valueOf(hireDateStr)); ps1.executeUpdate(); ResultSet keys ps1.getGeneratedKeys(); keys.next(); int empId keys.getInt(1); PreparedStatement ps2 conn.prepareStatement( INSERT INTO login(emp_id, username, password) VALUES(?,?,?)); ps2.setInt(1, empId); ps2.setString(2, username); ps2.setString(3, defaultPwd); ps2.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); e.printStackTrace(); } finally { conn.setAutoCommit(true); }这段代码里有两点值得注意第一prepareStatement时声明Statement.RETURN_GENERATED_KEYS后续才能通过getGeneratedKeys拿到自增主键很多新手不写这个参数拿到的主键全是0。第二hireDate需要先转成java.sql.Date否则JDBC会报日期类型转换错误或存入当前时间。5. 排班方案与线路-站点关系多表联查和事务保存5.1 排班表的设计一个员工一周跑哪些车排班管理是本项目里业务逻辑最重的一块它同时关联了线路、车辆、员工三类主数据。schedule表的常见字段是id、line_id、vehicle_id、emp_id、run_date、shift_no。在界面上展示排班列表时不能直接把这三张表的ID显示给用户看而是要通过JOIN查出名称。SELECT s.id, l.line_name, v.plate_no, e.emp_name, s.run_date, s.shift_no FROM schedule s LEFT JOIN line l ON s.line_id l.id LEFT JOIN vehicle v ON s.vehicle_id v.id LEFT JOIN employee e ON s.emp_id e.emp_id ORDER BY s.run_date DESC, s.shift_no;这里用LEFT JOIN而不是INNER JOIN是因为历史排班数据可能关联的车辆或员工已经被删了如果使用INNER JOIN这条历史记录会直接消失不利于运营回溯。查询结果加载到JTable后遇到NULL值的地方可以显示“已删除”增加可读性。5.2 设置线路-站点顺序的界面技巧线路和站点是多对多关系一条线路要经过多个站点一个站点也可能被多条线路使用。因此需要route_station这张关联表来维护它们的关系其中station_order字段用来记录站点在线路上的先后顺序。网上很多项目会忽略这个顺序字段导致只管连接关系不管先后但公交场景里站点顺序是不可或缺的。界面常见做法是左侧JTable显示当前线路的站点右侧按钮控制上移、下移、添加、移除。保存时先删除这条线路的所有关联再按当前列表顺序重新插入。这种“先删后插”的方式虽然简单但能保证顺序和界面显示一致。String deleteSql DELETE FROM route_station WHERE line_id ?; PreparedStatement psDel conn.prepareStatement(deleteSql); psDel.setInt(1, lineId); psDel.executeUpdate(); for (int i 0; i stationList.size(); i) { String insertSql INSERT INTO route_station(line_id, station_id, station_order) VALUES(?, ?, ?); PreparedStatement psIns conn.prepareStatement(insertSql); psIns.setInt(1, lineId); psIns.setInt(2, stationList.get(i).id); psIns.setInt(3, i 1); psIns.executeUpdate(); }参数说明stationList是界面上显示顺序的列表i1表示序号从1开始。先删后插的好处是逻辑简单不用判断哪些记录要新增、哪些要删除坏处是每次修改都会产生多余的删除插入记录但对于课程设计级别的数据量来说完全可接受。如果需要更优雅的方案可以对新旧列表做差集只更新变化的部分但复杂度会明显上升。5.3 保存排班失败的五种原因排班保存按钮是出现问题最多的地方结合我自己的经验这里有一个容易踩坑的对照表方便你排查。界面操作数据库字段常见错误JComboBox选择车辆vehicle_id把显示的“1-大巴”字符串直接存库日期控件run_date误用java.util.Date直接setObject保存按钮schedule所有字段没有commit数据未落库同一辆车同一天排班vehicle_id run_date没有做唯一性校验下拉列表选员工emp_id把离职员工也带入排班前两个是最典型的。JComboBox里需要用两个数组一个存显示值一个存对应ID保存时通过getSelectedIndex从ID数组里取值而不是从选中文本里解析。日期控件建议直接使用JDatePicker或把字符串转成java.sql.Date避免时区导致日期偏移一天。6. 把项目跑起来驱动配置、时区、中文乱码和打包技巧6.1 从压缩包到运行的三步配置下载的压缩包解压后第一件事不是双击Login.java而是按顺序检查环境。第一步确认JDK是1.8.0_201第二步在IDEA 2021.3.3里把mysql-connector-java-8.0.29.jar添加为库第三步导入busmanage.sql。在IDEA中导入jar包后一定要在Project Structure里确认它出现在Libraries列表里否则运行时Class.forName找不到驱动。mysql -uroot -p -e source /path/busmanage.sql如果在命令行导入失败优先检查文件编码。用记事本把busmanage.sql另存为UTF-8编码再重新导入。MySQL 8.0对UTF-8的处理更严格带BOM头反而容易报错可以另存为无BOM的UTF-8。6.2 连接数据库的排查清单连接不上数据库时不要直接怀疑Java代码按这个顺序排查更快先ping端口telnet localhost 3306不通说明MySQL服务没启动。检查url里的数据库名busmanage是否存在大小写和实际库名一致。检查驱动jar版本是否跟MySQL版本匹配连8.0库用8.x驱动连5.7库用5.1.49或兼容包。检查连接串是否包含serverTimezone参数MySQL 8.0没有该参数会报时区异常。刚才说的All排查下来后一般能看到登录窗口。如果登录报Access denied那就是用户名或密码错了去MySQL终端用同样的账号密码验证一下别忽略空格。6.3 数据库连接信息外置到配置文件项目原版的数据库连接串可能直接写在DBUtil.java里如果要把这个项目变成可交付的成果建议把JDBC连接参数提取到db.properties文件。这样其他人拿到项目后不需要改Java代码只改配置文件就能连接自己的数据库。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/busmanage?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.userroot jdbc.password你的密码Java加载方式是用Properties读取文件再拼接URL这样打包成jar后修改配置也不用重新编译。如果你使用的是MySQL 8.0.13以上版本连接时如果碰到Public Key Retrieval is not allowed可以在URL末尾加上allowPublicKeyRetrievaltrue。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。