
简介基于Java Swing与MySQL实现的仓库管理系统设计源码面向Java初、中级开发者及需要快速搭建仓储管理应用的学生与团队覆盖商品入库、出库、库存查询、权限控制与报表生成等核心环节。压缩包共39个文件、约8.45MB以15个Java源文件为核心配合10个JAR运行依赖包、7个XML配置文件、Git忽略文件、IML与属性配置以及说明文档和图片工程结构完整便于导入IDE运行和二次修改。目前已有118人浏览学习。源码按数据访问层、实体类、工具类、界面框架与配置模块分层组织弹窗示例和组件示例直观展示Swing布局与交互方式研读DAO层与事务处理代码还可掌握MySQL表设计和仓库管理业务流程的落地方法整体可读性强适合课程设计、毕业设计或小型仓储项目参考。1. 仓库管理系统这套SwingMySQL源码为什么至今仍是练手首选如果只给你三天时间交一个能演示的仓库管理系统用 Java Swing 加 MySQL 是大多数人兜底最稳的路线。Swing 不需要额外的前端框架和 Node 环境MySQL 本地装完就能建库整套源码从建表语句到界面事件都摊在明面上改动范围可控不像接一个前后端分离的工业级项目那样有黑匣子。这套组合解决的是中小仓库最朴素的需求商品建档、入库、出库、库存查询和基础统计数据量在几万行以内完全撑得住。它适合三类人做课程设计的学生想给内网团队做桌面工具的后端开发以及第一次接触桌面数据库应用、想把 Java 基础串起来的自学者。这篇就按我实际搭过的思路把表设计、JDBC 连接、Swing 交互和排查顺序一次讲清。2. 先设计表结构再动手写界面四个核心表与数据流向2.1 分三层的源码结构界面、业务、数据访问各管什么拿到一份“设计源码”最怕的是上千行代码全挤在 JFrame 子类里。Swing 的写法天然鼓励“把逻辑写在按钮监听器里”因为这样离数据最近写完立刻能跑。但仓库管理这个题目商品、库存、流水、用户四类数据互相牵制全堆在一起后期改一个字段要翻三四个类改完还会互相踩。我一般把源码按三层拆界面层放 JFrame、JPanel、对话框和 TableModel只负责展示和收集输入业务层放入库、出库、盘点这些操作负责参数校验和决定更新哪些表数据访问层放 SQL 和 JDBC 代码每类实体对应一个 DAO 类例如 ProductDao、StockDao、StockRecordDao。拆完之后最直接的收益是排查路径变短界面点按钮没反应先看监听器调用的业务方法某个 SQL 报错直接去 DAO 里找语句如果要换数据库方言只需要动数据访问层。初学者最容易犯的错是把 DAO 层当成“写着玩的文件夹”结果业务逻辑里到处 new Connection换库的时候欲哭无泪。源码里只要出现 DAO 或者 Service 字样就应该坚持这个边界别让按钮监听器里出现 SELECT 开头的字符串。2.2 流水表加汇总表为什么库存表要拆成两张很多初版实现只建一张出入库明细表查询当前库存时用 SUM 现算数据量小看不出问题录上几万条流水后每次打开列表都明显变慢反过来只存一张当前库存表又是另一种翻车只 update 当前值历史明细全丢月底对账无据可查。仓库管理系统里最常见的正确做法是把两者都保留t_stock 只存每个商品当前有多少作为查询和判断库存是否足够的依据t_stock_record 记每一次入库、出库的明细作为追溯和报表依据。写代码时把“更新汇总表、插入流水表”放进同一个数据库事务里两张表不会出现不一致。这个设计的成本只有一张表的代码量收益却很实在。做库存列表时查 t_stock一行一个商品界面秒出做流水查询或按日期统计时查 t_stock_record按时间倒序分页。两个查询各司其职都不用做全表聚合。还有一个容易被忽略的细节流水表里必须记录变动前后的库存值 before_quantity 和 after_quantity这样将来就算汇总表被改错也能靠流水把账算回来这是给“后悔药”留的后门。2.3 建库建表脚本核心字段与外键一次到位-- 建库统一用 utf8mb4避免中文和生僻字乱码 CREATE DATABASE IF NOT EXISTS warehouse_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE warehouse_db; -- 系统用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码摘要示例用MD5正式环境换加盐哈希, real_name VARCHAR(50) COMMENT 姓名, role TINYINT NOT NULL DEFAULT 0 COMMENT 0管理员 1仓管员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB COMMENT系统用户表; -- 商品信息表 CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(32) NOT NULL UNIQUE COMMENT 商品编码, name VARCHAR(100) NOT NULL COMMENT 商品名称, category VARCHAR(50) COMMENT 分类, spec VARCHAR(100) COMMENT 规格型号, unit VARCHAR(10) DEFAULT 件 COMMENT 计量单位, price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 参考单价, safe_stock INT NOT NULL DEFAULT 0 COMMENT 库存预警下限, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT商品信息表; -- 库存汇总表一个商品一条记录只存当前量 CREATE TABLE t_stock ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL UNIQUE COMMENT 商品ID与商品一一对应, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_stock_product FOREIGN KEY (product_id) REFERENCES t_product(id) ) ENGINEInnoDB COMMENT库存汇总表; -- 出入库流水表每次变动一条记录用于追溯 CREATE TABLE t_stock_record ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL COMMENT 商品ID, record_type TINYINT NOT NULL COMMENT 1入库 2出库, quantity INT NOT NULL COMMENT 变动数量正数, before_quantity INT NOT NULL COMMENT 变动前库存, after_quantity INT NOT NULL COMMENT 变动后库存, operator_id INT COMMENT 操作人关联t_user.id, remark VARCHAR(255) COMMENT 备注如采购入库/销售出库, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_record_product FOREIGN KEY (product_id) REFERENCES t_product(id) ) ENGINEInnoDB COMMENT出入库流水表;这段脚本里有几个参数决定了后续能不能顺利跑起来。第一是 ENGINEInnoDB事务和行级锁都靠它MyISAM 不支持事务入库更新库存和写流水必须原子执行所以千万不能把引擎换成 MyISAM。第二是字符集建库时指定 utf8mb4比 utf8 多支持一部分字符连接 URL 里还要配合 characterEncodingutf8 才不会乱码。第三是外键product_id 被库存表和流水表引用能阻止插入不存在的商品记录代价是删除商品时会受约束这个坑在第 5 章单独讲。COMMENT 注释建议保留写代码时能看到字段含义后面生成报表也方便。2.4 初始化测试数据账号商品库存一次铺好建完表后库里是空的界面上什么都看不到。我习惯把测试数据直接写进一个 init.sql每次重装环境执行一遍就能恢复到可演示状态INSERT INTO t_user (username, password, real_name, role) VALUES (admin, MD5(123456), 系统管理员, 0); INSERT INTO t_product (sku_code, name, category, unit, price, safe_stock) VALUES (SKU-001, A4复印纸, 办公耗材, 箱, 89.00, 10), (SKU-002, 中性笔, 办公耗材, 盒, 12.50, 20), (SKU-003, 无线鼠标, 外设, 个, 45.00, 5); INSERT INTO t_stock (product_id, quantity) SELECT id, 50 FROM t_product;库存初始化用 SELECT id FROM t_product 而不是手写具体 ID这样无论商品表数据怎么调整库存始终和外键对齐。第三段的 SELECT 写法也适合在新增商品后自动补一条库存记录先插商品再执行一次 INSERT INTO t_stock SELECT id, 0 FROM t_product WHERE id 某个值。这里提前铺三条商品和初始库存是因为入库出库操作需要真实数据来演示空库状态下按钮点了没有反馈演示容易翻车。3. JDBC 连接 MySQL驱动参数、DBHelper 封装与查询的正确写法3.1 驱动版本与 URL 参数时区、SSL、编码一个都不能少先把连接参数讲清楚因为这段配置是整套源码里最容易报错的地方。MySQL 8.x 用的驱动类名是 com.mysql.cj.jdbc.Driver老教程里常见的 com.mysql.jdbc.Driver 是 5.x 时代的写法照抄到 8.x 驱动下类名不存在直接 ClassNotFoundException。我一般把 URL 拆成多行拼接每个参数单独占一行出问题也好排查private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/warehouse_db ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8 allowPublicKeyRetrievaltrue rewriteBatchedStatementstrue;几个参数的作用分别是useSSLfalse 关闭 SSL 握手本地开发环境不需要加密少了 SSL 相关告警serverTimezoneAsia/Shanghai 解决连接时报 “The server time zone value” 错误这是 8.x 驱动强制要求时区导致的characterEncodingutf8 和建库的 utf8mb4 配套保证中文在传输过程中不乱码allowPublicKeyRetrievaltrue 解决 MySQL 8 默认认证插件 caching_sha2_password 下连不上报 “Public Key Retrieval is not allowed” 的问题。rewriteBatchedStatementstrue 是可选的批量插入流水时性能提升明显数据量小的系统加了也无副作用。还有个细节URL 里多个参数用 连接但在 XML 配置文件里需要写成 如果是从配置中心读取就没事。这里把最常见的驱动连接报错整理成一张对照表排查时直接对号入座报错片段通常原因处理方法ClassNotFoundException: com.mysql.cj.jdbc.Driverjar 不是 8.x 驱动换驱动包并改用 cj 类名The server time zone value未指定时区URL 加 serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowed认证插件不兼容加 allowPublicKeyRetrievaltrueUnknown database warehouse_db库名拼错或未建库先执行第 2 章建库脚本Access denied for user账号密码或权限不对核对 USER/PASSWORD 和远程访问权限3.2 统一封装 DBHelper连接获取与资源释放连接管理最好集中在一个类里而不是每个 DAO 各写一套这样改数据库账号、换驱动、加日志都只动一个文件。一个够用的 DBHelper 长这样import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBHelper { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/warehouse_db ?useSSLfalseserverTimezoneAsia/Shanghai characterEncodingutf8allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败请检查mysql-connector的jar包); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, 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) { } } } }驱动的 Class.forName 放在 static 块里类第一次被加载时只执行一次省得每个 DAO 重复注册。close 方法接收三个资源对象内部逐个判空关闭谁先创建谁后关先关 ResultSet再关 Statement最后关 Connection这个顺序能避免底层连接池的占用。注意 USER 和 PASSWORD 直接写死在类里这套源码如果只在本机跑没问题要提交到团队协作或部署到内网建议改成从 config.properties 读取否则别人拿到源码第一件事就是改密码和 URL改完还可能因为编码问题读不到——这也是常见的翻车点。提示DBHelper 的字段全部用 private static final连接参数集中管理。开发机上先用本地测试库别直接拿生产库账号试。3.3 用 PreparedStatement 做增删改查别碰字符串拼接JDBC 里查询最忌讳的是拼 SQL比如String sql SELECT * FROM t_user WHERE username username 。这种写法在带登录功能的项目里纯属送人头输入框里填一个引号就能让语句语义改变还会遇到整型字段空值拼接报错。正确写法是预编译占位符public User findUser(String username, String password) { String sql SELECT id, real_name, role FROM t_user WHERE username ? AND password MD5(?); try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setRealName(rs.getString(real_name)); u.setRole(rs.getInt(role)); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }占位符从 1 开始编号setString 的参数顺序必须和 SQL 里问号的顺序一一对应顺序错了数据就查错人。这里把 MD5 求值放在 SQL 端也可以先在 Java 里算好摘要再 setString两种都能跑区别是前者把密码逻辑留在数据库。try-with-resources 语法会自动关闭 Connection 和 PreparedStatement省掉了 finally 里的样板代码这在小项目里很实用但要注意它只能管住 try 块内创建的连接如果连接是外面传进来的关闭时机还是要由调用方控制。每次查询都重新获取连接对这个体量的桌面应用完全没问题不需要为它提前引入连接池。4. Swing 界面这样搭主窗体、库存表格与入库出库操作4.1 主窗体骨架菜单、工具栏与选项卡布局Swing 界面第一刀切在主窗体结构上。仓库管理系统的功能就四块库存列表、入库登记、出库登记、流水查询用 JTabbedPane 做成选项卡比开四个 JFrame 窗口体验好得多窗口多了用户容易找不到任务栏也乱。我习惯把主窗体代码保持精简只负责组装面板public class MainFrame extends JFrame { public MainFrame() { setTitle(仓库管理系统); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(1024, 680); setLocationRelativeTo(null); JTabbedPane tabs new JTabbedPane(); tabs.addTab(库存列表, new StockPanel()); tabs.addTab(入库登记, new InboundPanel()); tabs.addTab(出库登记, new OutboundPanel()); tabs.addTab(流水查询, new RecordPanel()); setContentPane(tabs); } public static void main(String[] args) { SwingUtilities.invokeLater(() - new MainFrame().setVisible(true)); } }main 方法里套一层 SwingUtilities.invokeLater 是必须的Swing 组件只能在事件分发线程EDT上创建和修改直接 new JFrame 在偶发情况下会拿到不可见的窗口或者布局错乱。setSize 和 setLocationRelativeTo 组合让窗口居中显示1024x680 对库存列表和表单都够用。四个 Panel 各自是独立类哪里出问题改哪里后期加一个“盘点”选项卡只需新增一个 JPanel 子类并加一行 addTab。4.2 库存列表不卡界面JTable 配 TableModel 加 SwingWorker 刷新库存列表是整个系统的门面用户打开窗口第一眼就是它。直接用 JTable 默认模型也能显示数据但列名会变成 A、B、C列宽不可控所以要自定义 AbstractTableModel。刷新按钮的点击事件里如果直接执行 JDBC 查询数据库一慢整个窗口就变成“未响应”正确做法是把查询丢给 SwingWorker 后台线程public class StockPanel extends JPanel { private JTable table; private StockTableModel model; private JButton btnRefresh; public StockPanel() { setLayout(new BorderLayout()); model new StockTableModel(); table new JTable(model); table.setRowHeight(26); add(new JScrollPane(table), BorderLayout.CENTER); btnRefresh new JButton(刷新); btnRefresh.addActionListener(e - refreshAsync()); add(btnRefresh, BorderLayout.SOUTH); refreshAsync(); } private void refreshAsync() { btnRefresh.setEnabled(false); new SwingWorkerListObject[], Void() { Override protected ListObject[] doInBackground() { return new StockDao().findAll(); } Override protected void done() { try { model.setRows(get()); model.fireTableDataChanged(); } catch (Exception ex) { JOptionPane.showMessageDialog(StockPanel.this, 查询失败 ex.getMessage()); } finally { btnRefresh.setEnabled(true); } } }.execute(); } }doInBackground 在后台线程执行 JDBC 查询done 切回 EDT 更新表格这样查询耗时再久窗口也能正常拖动缩放。SwingWorker 的 execute 方式是异步的点击刷新后马上返回不会阻塞界面。配套的 TableModel 需要实现四个方法public class StockTableModel extends AbstractTableModel { private final String[] columns {商品编码, 商品名称, 分类, 库存, 单位, 参考单价}; private ListObject[] rows new ArrayList(); public void setRows(ListObject[] rows) { this.rows rows; } Override public int getRowCount() { return rows.size(); } Override public int getColumnCount() { return columns.length; } Override public String getColumnName(int col) { return columns[col]; } Override public Object getValueAt(int row, int col) { return rows.get(row)[col]; } }setRows 只是替换内存中的数据真正让表格重绘的是 fireTableDataChanged()它通知 JTable 行数可能变化需要重新渲染。数据用 Object[] 数组承载简单直接列顺序和 columns 数组严格对齐如果嫌数组不安全也可以改成 List getValueAt 里按列号手动取值。库存量这一列建议在 SQL 里就 CAST 成 INT让 getValueAt 返回数值类型否则默认右对齐和排序行为都会跑偏。4.3 入库出库的数据层一个事务里更新库存并写流水入库和出库是镜像操作核心逻辑只有一处不同入库数量为正出库数量为负。但无论哪种都要保证“库存表被更新”和“流水表被插入”同时成功或同时失败否则就会出现库存变了但没记录或者有记录但库存没变的脏账。使用事务的写法如下以入库为例public void doInbound(int productId, int quantity, int operatorId, String remark) throws SQLException { String sqlQuery SELECT quantity FROM t_stock WHERE product_id ? FOR UPDATE; String sqlUpdate UPDATE t_stock SET quantity quantity ? WHERE product_id ?; String sqlInsert INSERT INTO t_stock_record (product_id, record_type, quantity, before_quantity, after_quantity, operator_id, remark) VALUES (?, ?, ?, ?, ?, ?, ?); try (Connection conn DBHelper.getConnection()) { conn.setAutoCommit(false); int before; try (PreparedStatement psQuery conn.prepareStatement(sqlQuery)) { psQuery.setInt(1, productId); try (ResultSet rs psQuery.executeQuery()) { if (!rs.next()) { throw new SQLException(库存记录不存在请先初始化该商品的库存); } before rs.getInt(1); } } int after before quantity; try (PreparedStatement psUpdate conn.prepareStatement(sqlUpdate)) { psUpdate.setInt(1, quantity); psUpdate.setInt(2, productId); psUpdate.executeUpdate(); } try (PreparedStatement psInsert conn.prepareStatement(sqlInsert)) { psInsert.setInt(1, productId); psInsert.setInt(2, 1); psInsert.setInt(3, quantity); psInsert.setInt(4, before); psInsert.setInt(5, after); psInsert.setInt(6, operatorId); psInsert.setString(7, remark); psInsert.executeUpdate(); } conn.commit(); } catch (SQLException e) { throw new SQLException(入库失败已回滚 e.getMessage(), e); } }conn.setAutoCommit(false) 表示后续语句不再每条自动提交直到手动 commit。SELECT ... FOR UPDATE 把商品对应的库存行锁住防止两个人同时操作同一商品时算出相同的 before 值这在桌面应用多开窗口时尤其重要。若中途任何一条 SQL 抛异常try 块会退出导致 Connection 关闭未提交的事务被数据库自动回滚所以不需要显式 rollback 也安全。出库方法把 SQL 里的加号换成减号并在 after 小于 0 时抛异常拒绝执行同理可以整理共用一个 doStockChange(recordType) 方法。5. SwingMySQL 避坑指南五类高频问题的排查方法5.1 中文乱码建库、连接、编译三个环节逐个查现象界面上显示“???”或者从数据库查询出来的中文全是问号插入的中文也存成问号。原因往往不是单点而是三个环节至少一处断裂建库字符集不是 utf8mb4、连接 URL 没带 characterEncodingutf8、Java 源文件编译时用了平台默认编码。解决方法是逐一确认先看建库语句有没有 DEFAULT CHARACTER SET utf8mb4再看 URL 参数里 characterEncoding 是否为 utf8注意这里写 utf8 而不是 utf8mb4驱动按 utf8 传输即可覆盖最后检查 IDE 的编译选项把编码设为 UTF-8Maven 工程则要在配置里加 project.build.sourceEncoding。最省事的检验办法是执行一条 SELECT HEX(name) FROM t_product WHERE id1如果中文对应的十六进制是 E5 开头说明存储正常乱码出在显示层如果是 3F就是存储阶段已经坏了。5.2 Too many connections连接泄漏问题与排查命令现象程序跑一段时间后报 “Too many connections”重启又正常再过一会又报。原因绝大多数是 JDBC 连接没有被关闭常见于查询抛异常后 finally 里没关 Connection或者忘了关 PreparedStatement、ResultSet底层连接一直被占用。解决思路分两步。第一步用 SHOW PROCESSLIST 看当前连接状态重点找大量 Sleep 的连接它们的来源 IP 和端口通常集中在同一个进程说明是某个 DAO 方法反复拿连接却不还第二步检查所有 DAO 方法确保每条路径上都执行了 DBHelper.close 或使用 try-with-resources特别是 catch 分支里提前 return 的情况。这里有个经验不要在 finally 里只关 Connection 不关 Statement 和 ResultSet三样都要成对处理否则连接即使归还了语句资源还挂着。5.3 一操作界面就卡死JDBC 查询阻塞了事件线程现象点击刷新或登录按钮后整个窗口变成灰白色标题栏显示“未响应”过几秒才恢复。原因是按钮监听器直接在 EDT 上执行了 JDBC 查询数据库响应慢或网络有延迟时事件分发线程被阻塞重绘、鼠标事件全部排队。解决方法是把所有数据库操作放进后台线程最简单的就是第 4 章用到的 SwingWorkerdoInBackground 执行查询done 里更新组件。另一个容易忽略的点是像 JOptionPane.showMessageDialog 这类模态对话框要留在 EDT 上调用别放进 doInBackground否则弹窗出现在错误的事件上下文。判断方法很简单在查询前打印一条日志如果窗口卡住时点击其他按钮没有任何反应基本就是 EDT 被占用。5.4 商品删不掉外键约束与删除顺序现象删除一个商品时 SQL 报错 “Cannot delete or update a parent row: a foreign key constraint fails”。原因t_product 被 t_stock 和 t_stock_record 的外键引用只要库存表或流水表里还有这个商品的记录主表就不能直接删。解决方法是按依赖关系倒着删先删除 t_stock_record 里该商品的所有流水再删除 t_stock 里的库存记录最后删 t_product。如果业务上不允许删除历史流水常见做法是给商品表加一个 is_deleted 字段删除操作变成 UPDATE is_deleted1查询列表时默认过滤掉这就是逻辑删除。桌面系统里逻辑删除的成本很低而且保留了完整历史对仓库这种需要审计的场景更有价值。另一个相关坑是新增商品时忘了同步插库存记录入库操作就会报“库存记录不存在”解决方法是把商品新增和库存初始化的两个 INSERT 放在同一个事务或同一个 DAO 方法里。5.5 驱动版本与 MySQL 不匹配连跑都跑不起来的典型报错现象启动程序就报 “ClassNotFoundException: com.mysql.cj.jdbc.Driver” 或 “Communications link failure”前者是驱动类不存在后者是协议版本不兼容。原因工程里放的是 5.x 的 mysql-connector-java 驱动包却连了 MySQL 8.x 数据库或者反过来驱动太老不认识新版数据库的认证插件。解决方法是统一成 MySQL 8.x 时代的驱动类名用 com.mysql.cj.jdbc.Driver导入 jar 时注意别出现两个版本的驱动包同时出现在 classpath这种冲突最隐蔽编译不报错运行才炸。检查驱动的快捷方式是在 IDE 的依赖列表里搜 mysql确认只有一个 jar版本和数据库实例大版本一致。数据库服务端如果是 5.7用 8.x 驱动也能向下兼容反而更稳。6. 从能跑到能演示代码整理、密码加盐与导出报表的小技巧源码能跑只是及格线演示和答辩看的是代码习惯。三个改动不大但加分明显的小技巧值得做。第一把所有 SQL 字符串收进 DAO 类界面层看不到一条 SELECT检查代码的人一眼就能看出分层意识顺带把散落的 System.out.println 换成统一日志输出没引日志库就先用 e.printStackTrace() 打到控制台也比到处是调试输出强。第二登录密码不要用裸 MD5演示代码里为了省事可以用但要在注释里写明正式场景要换加盐哈希比如 SHA-256 加随机盐盐存单独字段校验时读出来重新计算对比。第三加一个“导出 CSV”按钮仓库管理的报表是高频需求用 StringBuilder 拼行比用 Excel 库轻量得多public static void exportCsv(ListObject[] rows, File file) throws IOException { StringBuilder sb new StringBuilder(); sb.append(\uFEFF); // 加BOM防止Excel打开中文乱码 sb.append(商品编码,商品名称,当前库存,单位\n); for (Object[] r : rows) { sb.append(r[0]).append(,).append(r[1]) .append(,).append(r[2]).append(,) .append(r[3]).append(\n); } Files.write(file.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8)); }CSV 开头写一个 \uFEFFBOMExcel 才能正确识别 UTF-8 编码不写的话双击打开全是乱码这个细节能省下一堆答疑时间。字段内容如果包含逗号和引号记得用双引号包一层并转义内部引号否则导出文件列会错位。导出前先跑一遍完整入库出库流程确认数据总量和时间范围演示时打开导出的文件核对几条关键的出入库记录这一套验证下来基本不会在台上翻车。我最早做这套方案时把 SQL 全写在按钮监听器里改一个报表需求要从界面类找到数据库层前后改了一整晚后来老老实实按 DAO 分层才算止损。仓库管理系统用 Swing 加 MySQL 不新但胜在链路短、边界清楚把表结构和事务这两块地基打牢后续加什么功能都接得住。希望帮到你。本文还有配套的精品资源点击获取