多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java Swing+MySQL物资信息管理系统全解析:从建库到答辩

Java Swing+MySQL物资信息管理系统全解析:从建库到答辩 简介面向Java课程设计与期末大作业场景的物资信息管理系统完整项目以Java Swing构建桌面端界面、MySQL作为数据存储并配有数据库脚本与设计文档。系统覆盖登录认证、用户管理、基础信息维护、物资出入库、查询统计等典型模块难度适中且可本地编译运行适合需要完整参考方案的开发者直接借鉴。资源包共34个文件包含27个Java源码、3个SQL脚本建库、存储过程、触发器、Word格式系统设计说明及Markdown说明文档压缩包约4.74MB目录按源码、数据库和文档分层组织便于按需查阅。除了可运行源码外还附带数据库设计文档、物资管理子系统zip包等能帮助理解表结构、存储过程与触发器实现。已有133人学习可作为课程设计、期末大作业或毕业设计前的基础演练项目。1. 物资信息管理系统这个“高分项目”到底在考什么毕业设计或课程设计答辩前一周很多同学手里拿到的就是这样一套东西JavaSwingMysql的物资信息管理系统源码文档说明界面能启动数据能增删改查但被老师问一句“你这张库存表为什么单独建一张”就卡壳。这个题目不是冷门偏题恰恰相反它是国内高校软件工程、信息管理专业里出现频率最高的课设组合之一。“高分项目”四个字浓缩了评分标准本身功能完整度、数据库设计规范性、代码分层清晰度、文档能不能把设计思路讲明白。这篇笔记适合两类人一是第一次写管理系统、想把现成源码吃透再改造成自己题目的在校生二是想快速复用一个经典课设骨架做二次开发的从业者。读完你应该能回答这套系统为什么由这几部分组成、从建库到跑通窗口的最小步骤是什么、调试和答辩时最容易在哪些地方翻车。2. 为什么还是 JavaSwingMysql技术选型与业务建模2.1 从评分表倒推技术选型Swing 为何在课程设计里仍是主力这几年 SpringBoot Vue 成了企业开发的主流因此很多人拿到“物资信息管理系统”这个题目时第一反应是“都什么年代了还用 Swing” 但如果你认真看过课程设计的评分表会发现评委关注的点并不是技术栈新旧而是下面四个能力点面向对象封装是否合理、JDBC 与 SQL 基本功是否扎实、界面与数据能否联动、文档能否支撑答辩。Swing 恰好把这四个能力点都暴露在代码表面不需要额外引入前端工程化和后端容器老师打开项目就能看到分层结构。Swing 的第二个优势是部署和演示成本极低。一台装了 JDK 的机器就能跑不用配置 Tomcat、不用处理跨域、不用等前端 npm install。答辩现场最常见的尴尬是网络不通或端口被占用桌面程序没有这类外部依赖。反观前后端分离项目光环境变量就能折腾半小时而这些时间和评分无关。对比项SpringBoot Vue 前后端分离Java Swing MySQL 桌面端环境依赖JDK Maven Node 数据库需配置后端服务和前端构建只需要 JDK MySQLIDE 直接运行演示方式浏览器操作需要后端服务在线本地窗口直接演示离线可用评分可见度业务逻辑藏在接口层评委需要追问MVC 分层肉眼可见类职责直观答辩追问点Redis、网关、鉴权等框架概念JDBC、事务、SQL 设计等基础原理改造工作量换题目需要改接口和页面两处换表结构和几个面板即可还有一个容易被忽略的现实因素指导老师对 Swing 项目太熟悉了他能准确判断哪些代码是你写的、哪些是网上复制粘贴改了个类名。JavaSwingMysql 的物资管理系统不是为了炫技而是为了把“我会写 Java 代码、会设计表、会做界面”这件事在十分钟内证明完。如果你非要用 Spring Cloud 微服务做物资管理大概率会被反问“为什么一个实验室规模的项目要上注册中心”到时候反而难收场。2.2 模块拆分与数据库设计六张表把物资流转说清楚物资信息管理系统的业务闭环可以用一句话概括物资从哪来、放到哪、被谁领走、还剩多少。拆成功能模块就是四块基础资料供应商、物资类别、出入库业务入库单、出库单、库存台账、查询统计。很多低分项目的问题恰恰是模块堆得太散或者把所有内容塞进一两张表里硬撑。我一般会建议按“单据 明细 台账”的经典结构来拆表。单据是业务发生的一次记录比如一次入库对应一条主记录主记录下面挂多条物资明细明细表记录每种物资入库的数量台账是当前实时库存。为什么要把当前库存单独落到一张表而不是每次用 SUM(入库明细) - SUM(出库明细) 现场算因为物资管理系统是给库管员用的查询库存的频率远高于出入库实时计算随着数据量增大越来越慢而台账表的维护成本只是一次事务里多执行一条 UPDATE。这也顺带解释了为什么库存字段要设置默认值 0而不是允许 NULLNULL 参与算术运算会直接把结果变成 NULL改默认值能用最简单的方式规避一类低级 bug。表与表的关系大致是这样物资类别表是一张字典表被物资表外键引用供应商表与入库主表是一对多物资表与入库明细、出库明细都是一对多最后库存表与物资表一对一。外键要不要真的在数据库层面声明我建议建表时加上虽然很多老手为了扩展性会故意去掉外键但课设评分标准里通常有“完整性约束”这一项有外键和索引能直接看到设计意图。字段类型上数量用 DECIMAL 而不是 INT因为现实中存在按重量或体积计量的物资金额字段同样 DECIMAL(10,2)时间字段用 DATETIME 并默认 CURRENT_TIMESTAMP避免代码里手动拼时间字符串。3. 从建库到跑通第一个窗口项目骨架搭建的完整步骤3.1 建库建表一次性写对物资信息管理系统的 SQL 脚本拿到源码的第一步不是读代码而是先把数据库环境准备干净。我习惯把建库脚本和示例数据分开写建库脚本只负责结构演示数据单独一个 SQL 文件这样评审要数据库设计文档的时候可以直接交付结构脚本。下面是物资管理系统最基础的一套建表语句覆盖前面说的六张表物资类别、供应商、物资、入库主表、入库明细、出库主表、出库明细、库存表一共八张其中出库相关结构对称于入库。-- 创建数据库统一使用 utf8mb4避免中文乱码 CREATE DATABASE IF NOT EXISTS material_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE material_db; -- 物资类别表 CREATE TABLE t_category ( category_id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL UNIQUE, remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 供应商表 CREATE TABLE t_supplier ( supplier_id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20), address VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 物资表 CREATE TABLE t_material ( material_id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, material_no VARCHAR(50) NOT NULL UNIQUE, material_name VARCHAR(100) NOT NULL, spec VARCHAR(100), unit VARCHAR(10), price DECIMAL(10,2) DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_material_category FOREIGN KEY (category_id) REFERENCES t_category(category_id) ) ENGINEInnoDB; -- 入库主表 CREATE TABLE t_inbound ( inbound_id INT PRIMARY KEY AUTO_INCREMENT, inbound_no VARCHAR(30) NOT NULL UNIQUE, supplier_id INT, inbound_date DATETIME DEFAULT CURRENT_TIMESTAMP, operator VARCHAR(50), remark VARCHAR(200), CONSTRAINT fk_inbound_supplier FOREIGN KEY (supplier_id) REFERENCES t_supplier(supplier_id) ) ENGINEInnoDB; -- 入库明细表 CREATE TABLE t_inbound_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, inbound_id INT NOT NULL, material_id INT NOT NULL, quantity DECIMAL(12,2) NOT NULL, unit_price DECIMAL(10,2) DEFAULT 0.00, CONSTRAINT fk_item_inbound FOREIGN KEY (inbound_id) REFERENCES t_inbound(inbound_id), CONSTRAINT fk_item_material FOREIGN KEY (material_id) REFERENCES t_material(material_id) ) ENGINEInnoDB; -- 库存表单独维护当前库存避免每次 SUM 明细 CREATE TABLE t_stock ( stock_id INT PRIMARY KEY AUTO_INCREMENT, material_id INT NOT NULL UNIQUE, stock_qty DECIMAL(12,2) NOT NULL DEFAULT 0, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_stock_material FOREIGN KEY (material_id) REFERENCES t_material(material_id) ) ENGINEInnoDB;这段脚本里有几个细节值得注意。每张表都指定了 InnoDB 引擎因为后面入库和出库要做事务MyISAM 不支持行级事务选了它等于放弃回滚能力。字符集统一用 utf8mb4 而不是 utf8因为物资名称和备注里可能存 emoji 或生僻字utf8mb4 是 utf8 的超集兼容性更好。库存字段用了 DEFAULT 0 而不是允许 NULL这样任何一条物资在入库前就已经有了初始行后续更新库存时不需要先判断该行是否存在。最后外键名都显式赋予了可读名字例如 fk_stock_material这种命名方式在数据库设计文档里可以直接被评审引用比系统自动生成的随机名清楚得多。3.2 封装 JDBC 连接一个 DBUtil 类解决驱动、连接与关闭Swing 项目里最常见的坏味道就是每个业务类里都出现一遍DriverManager.getConnection、Connection、PreparedStatement、ResultSet四件套然后记得关这个忘了关那个。我会在第一版就写一个 DBUtil 类把连接和释放收口。JDBC 连接不是免费的每次创建都要经过 TCP 握手和认证虽然桌面程序并发不高但至少要让代码结构经得起追问。import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { // 换成你本机的地址、端口、库名 private static final String URL jdbc:mysql://localhost:3306/material_db ?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { // MySQL 8.x 驱动类名5.x 是 com.mysql.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement st, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { } } if (st ! null) { try { st.close(); } catch (SQLException e) { } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { } } } }这段代码的逻辑并不复杂静态块在类加载时注册 JDBC 驱动getConnection 每次返回一个物理连接close 方法按照 ResultSet、Statement、Connection 的顺序倒序释放。为什么必须倒序因为 ResultSet 依赖 StatementStatement 依赖 Connection正序关闭会先把地基拆了依赖它的对象在 close 时可能抛异常。URL 里的三个参数是被坑出来的useSSLfalse关掉 MySQL 8 默认的 SSL 握手characterEncodingutf8保证中文不乱码serverTimezoneAsia/Shanghai解决驱动与本地时区不一致导致的日期偏移。实际项目中如果把密码写死在类里不合适可以放到 properties 文件用 ResourceBundle 读取但在课设阶段写常量反而更容易让读者理解。关于Class.forName这行代码几乎每次答辩和 Java 面试题里都会被拿出来问为什么要注册驱动JDBC 规范要求驱动类必须提供一个静态初始化块来向 DriverManager 注册自己而 Class.forName 的作用是触发类加载和静态块执行。如果使用较新的 JDBC 4.0 驱动可以省略这行但写上它属于最保险的习惯也能让旧版 MySQL 5.7 驱动继续工作。3.3 用 Swing 搭出主窗口骨架菜单栏、工具栏与内容面板数据库准备完成后下一步是搭一个能承载所有业务功能的主窗口。一个典型的物资管理系统主界面由三部分构成顶部菜单栏负责入口导航例如“基础资料”“入库管理”“出库管理”中部用 JTabbedPane 承载多个业务面板切换方便。这样整个程序不需要多个 JFrame 来回跳也避免“开了三个窗口不知道哪个是最新数据”的混乱。import javax.swing.*; public class MainFrame extends JFrame { public MainFrame() { setTitle(物资信息管理系统); setSize(1000, 700); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 菜单栏 JMenuBar menubar new JMenuBar(); JMenu menuBase new JMenu(基础资料); JMenuItem itemCategory new JMenuItem(物资类别管理); JMenuItem itemMaterial new JMenuItem(物资管理); menuBase.add(itemCategory); menuBase.add(itemMaterial); JMenu menuBiz new JMenu(出入库); JMenuItem itemInbound new JMenuItem(入库登记); JMenuItem itemOutbound new JMenuItem(出库登记); menuBiz.add(itemInbound); menuBiz.add(itemOutbound); menubar.add(menuBase); menubar.add(menuBiz); setJMenuBar(menubar); // 中间用选项卡切换各个面板 JTabbedPane tabs new JTabbedPane(); tabs.addTab(入库登记, new InboundPanel()); tabs.addTab(出库登记, new OutboundPanel()); tabs.addTab(库存查询, new StockQueryPanel()); add(tabs); // 菜单项点击后切换对应选项卡 itemInbound.addActionListener(e - tabs.setSelectedIndex(0)); itemOutbound.addActionListener(e - tabs.setSelectedIndex(1)); } public static void main(String[] args) { SwingUtilities.invokeLater(() - { new MainFrame().setVisible(true); }); } }这段骨架里有两个值得在答辩时展开的细节。一是SwingUtilities.invokeLater把界面创建放到事件调度线程中执行Swing 组件不是线程安全的所有界面更新都必须在 EDT 上完成否则偶发性界面卡死就是典型的线程问题。二是菜单点击事件用 lambda 表达式写Java 8 后 ActionListener 是函数式接口lambda 比匿名内部类少很多样板代码。addTab方法里的 InboundPanel、OutboundPanel、StockQueryPanel 是后续要实现的业务面板先在 MainFrame 里占好位置后面每实现一个类就能立刻在这里看到效果。4. 物资入库、出库与库存查询核心业务的实现套路4.1 入库登记事务、批量插入与库存联动入库是整个系统的核心写入动作它要同时完成三件事写入入库主表、写入入库明细表、更新库存表。任何一个环节失败前面写入的数据都不应该留下半截否则库存和单据对不上。这正是数据库事务要解决的问题也是物资管理系统里最值得展开讲的代码片段。public boolean doInbound(InboundDto dto) throws SQLException { ListInboundItemDto items dto.getItems(); if (items null || items.isEmpty()) { throw new IllegalArgumentException(入库明细不能为空); } Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 开启事务 // 1. 写入入库主表 String sqlInbound INSERT INTO t_inbound (inbound_no, supplier_id, operator, remark) VALUES (?, ?, ?, ?); PreparedStatement ps conn.prepareStatement(sqlInbound, Statement.RETURN_GENERATED_KEYS); ps.setString(1, dto.getInboundNo()); ps.setInt(2, dto.getSupplierId()); ps.setString(3, dto.getOperator()); ps.setString(4, dto.getRemark()); ps.executeUpdate(); // 取自动生成的主键供明细表外键引用 ResultSet keys ps.getGeneratedKeys(); int inboundId 0; if (keys.next()) { inboundId keys.getInt(1); } keys.close(); ps.close(); // 2. 逐条插入明细同时更新库存 for (InboundItemDto item : items) { String sqlItem INSERT INTO t_inbound_item (inbound_id, material_id, quantity, unit_price) VALUES (?, ?, ?, ?); PreparedStatement psItem conn.prepareStatement(sqlItem); psItem.setInt(1, inboundId); psItem.setInt(2, item.getMaterialId()); psItem.setBigDecimal(3, item.getQuantity()); psItem.setBigDecimal(4, item.getUnitPrice()); psItem.executeUpdate(); psItem.close(); // 库存表按物资主键更新不存在则插入UPSERT 思路 String sqlStock INSERT INTO t_stock (material_id, stock_qty) VALUES (?, ?) ON DUPLICATE KEY UPDATE stock_qty stock_qty VALUES(stock_qty); PreparedStatement psStock conn.prepareStatement(sqlStock); psStock.setInt(1, item.getMaterialId()); psStock.setBigDecimal(2, item.getQuantity()); psStock.executeUpdate(); psStock.close(); } conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { conn.rollback(); // 任何一步失败整单回滚 throw e; } finally { conn.setAutoCommit(true); // 恢复默认避免影响后续连接复用 conn.close(); } }这段代码里最关键的是事务边界的把握setAutoCommit(false)之后所有 SQL 都在同一个事务里直到commit()才真正落盘任何异常进入catch分支后执行rollback()。注意finally里把setAutoCommit(true)恢复回去这行容易被漏掉。因为连接池在真实项目中会复用连接如果连接带着自动提交 false 的状态回到池里下次使用的人会陷入莫名的不提交或不回滚。事务的粒度应该是一整单而不是每条明细单独一个事务——如果你把 commit 写在循环里第一条入库成功、第五十条失败库存就产生了 49 条无效增量这就是网上一些源码里“入库数量对不上”的根源。这里还用了ON DUPLICATE KEY UPDATE处理库存行不存在的场景。它的依据是 t_stock 表 material_id 上建了 UNIQUE 约束重复插入时数据库会识别冲突并执行后面的累加逻辑。相比先 SELECT 再判断 UPDATE这种方式把两步 SQL 合并成一步天然避免了判断与更新之间的间隙。4.2 出库与库存扣减用 where 条件拦截负数库存出库逻辑与入库对称但多了一个关键约束库存不能为负。有些人的实现是先SELECT stock_qty FROM t_stock WHERE material_id?在 Java 里判断数量充足再执行 UPDATE。这个逻辑在单用户演示时没问题但在真实的库管场景里隐藏着竞态条件两个人同时领用同一种物资两个线程都读到库存还剩 10甲扣了 8 剩 2乙也按 10 来扣 8最终库存变成 -6。解决办法是把判断下推到 SQL 的 WHERE 条件里。public boolean doOutbound(OutboundDto dto) throws SQLException { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); String sqlOutbound INSERT INTO t_outbound (outbound_no, receive_dept, operator, remark) VALUES (?, ?, ?, ?); // ... 写入出库主表代码同入库省略 for (OutboundItemDto item : dto.getItems()) { // 扣减库存且要求扣减后仍 0 String sqlDeduct UPDATE t_stock SET stock_qty stock_qty - ? WHERE material_id ? AND stock_qty ?; PreparedStatement ps conn.prepareStatement(sqlDeduct); ps.setBigDecimal(1, item.getQuantity()); ps.setInt(2, item.getMaterialId()); ps.setBigDecimal(3, item.getQuantity()); int rows ps.executeUpdate(); ps.close(); // 受影响行数为 0说明库存不足整单回滚 if (rows 0) { conn.rollback(); throw new StockNotEnoughException(库存不足物资编号: item.getMaterialId()); } // 写入出库明细 // ... 同入库明细写入逻辑省略 } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }执行 UPDATE 后返回的rows是数据库实际修改的行数这是判断扣减是否成功的最直接手段。库存在 WHERE 条件里被再次引用一旦剩余数量小于扣减数量数据库不会执行更新影响行数为 0业务层立刻抛异常并回滚整单。这个模式把并发控制交给数据库行锁比 Java 层 synchronized 可靠得多。需要提醒的是stock_qty ?中第二个参数是本次扣减量与 SET 子句中的扣减量相同很多新手漏掉这个条件只写WHERE material_id ?库存负数就会出现。面试官如果追问“如何防止超卖”这段代码就是标准答案。4.3 查询与排序SQL 拼装、时间范围与 JTable 排序库存查询是使用频率最高的功能也最容易写成两种极端要么把所有数据一次查出来在 Java 里过滤要么拼接 SQL 时不管什么条件都硬拼导致语法错误。正确做法是构建一个支持多个可选条件的查询方法条件存在才加入 WHERE参数一律用 PreparedStatement 的?占位。public ListStockVo queryStock(Integer categoryId, String materialName, Date startTime, Date endTime) throws SQLException { StringBuilder sql new StringBuilder(); sql.append(SELECT m.material_no, m.material_name, c.category_name, ) .append(s.stock_qty, m.unit, m.price ) .append(FROM t_stock s ) .append(JOIN t_material m ON s.material_id m.material_id ) .append(LEFT JOIN t_category c ON m.category_id c.category_id ) .append(WHERE 1 1 ); // 占位后续条件逐个追加 ListObject params new ArrayList(); if (categoryId ! null) { sql.append(AND m.category_id ? ); params.add(categoryId); } if (materialName ! null !materialName.trim().isEmpty()) { sql.append(AND m.material_name LIKE ? ); params.add(% materialName.trim() %); } if (startTime ! null) { sql.append(AND m.create_time ? ); params.add(new java.sql.Timestamp(startTime.getTime())); } if (endTime ! null) { sql.append(AND m.create_time ? ); params.add(new java.sql.Timestamp(endTime.getTime())); } sql.append(ORDER BY s.updated_time DESC); Connection conn DBUtil.getConnection(); try { PreparedStatement ps conn.prepareStatement(sql.toString()); for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); // 占位符从 1 开始 } ResultSet rs ps.executeQuery(); ListStockVo list new ArrayList(); while (rs.next()) { StockVo vo new StockVo(); vo.setMaterialNo(rs.getString(material_no)); vo.setMaterialName(rs.getString(material_name)); vo.setCategoryName(rs.getString(category_name)); vo.setStockQty(rs.getBigDecimal(stock_qty)); vo.setUnit(rs.getString(unit)); vo.setPrice(rs.getBigDecimal(price)); list.add(vo); } rs.close(); ps.close(); return list; } finally { conn.close(); } }这段查询有几点实现细节值得展开。动态条件用WHERE 1 1开头是一个常见技巧后面的条件全部以AND连接省去了判断“这是不是第一个条件”的麻烦。参数收集到 List 里循环用setObject设置这样新增条件时只需要加sql.append和params.add两行不会出现条件与占位符错位。LIKE 查询在参数里直接拼好%不要写% || ? || %这种数据库方言。时间参数转成java.sql.Timestamp而不是直接传java.util.Date这是 JDBC 规范的要求直接传 util.Date 在 MySQL 驱动下可能报参数类型不匹配。关于排序数据库里已经做了ORDER BY updated_time DESC这是主要排序依据。如果还想让用户点击表格列头排序可以在 JTable 上应用一行代码TableRowSorterTableModel sorter new TableRowSorter(table.getModel()); table.setRowSorter(sorter);。它会监听到表头点击事件在 JVM 内存里对当前页数据执行 java 排序排序逻辑发生在内存中仅影响当前展示顺序不改变数据库里的数据。注意加了 sorter 之后不要再通过getModel().getValueAt(row, col)拿数据因为表格行序已经被 sorter 重排正确方式是先table.convertRowIndexToModel(row)转换索引再取数据这个坑几乎每个做过 Swing 表格的人都会踩一次。5. SwingMySQL 的常见坑与排查从连不上库到中文乱码5.1 驱动类找不到ClassNotFoundException 背后的 classpath 问题现象程序一启动就报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver或者 IDE 里运行正常但打成 jar 包后双击就报错。原因MySQL 驱动的 jar 包没有真正进入运行时 classpath。IDE 里能跑是因为开发工具在项目配置里加了库引用但 jar 没有被打进最终产物或者团队里有人用 Maven 却忘记加 mysql-connector-java 依赖。解决先确认 jar 是否在本地依赖目录再检查 IDE 的 Module 设置。打包时如果直接用jar命令需要把 mysql-connector-java 的路径写进 Manifest 的 Class-Path用 Maven 则要加上mysql-connector-java依赖并把 scope 设为默认的 compile。判断驱动类是否在 classpath 里的最快方式是在命令行执行jar tf mysql-connector-java-8.0.x.jar | grep Driver能看到输出说明 jar 本身没问题问题在引用路径。这个坑通常发生在换了电脑或换了 IDE 之后有人从压缩包解压源码但 lib 目录里的 jar 没被成功导入。5.2 中文到处是问号连接串和数据库字符集要一起改现象界面按钮是中文正常但数据库里查出来的物资名称、备注全部是???或者反过来库里数据正常Swing 表格里显示乱码。原因字符集问题往往是三层中某一层不一致。数据库是 latin1或者表是 latin1连接串没带characterEncodingutf8SQL 文件本身以 GBK 保存导入时被错误解析。Swing 默认字符集与 JVM 启动参数也有关系但大多数情况下前两层就能解决八成问题。解决三层统一。建库时用utf8mb4并确认表继承该字符集JDBC URL 加characterEncodingutf8SQL 文件用 UTF-8 编码保存并在执行source之前执行SET NAMES utf8mb4;。改完之后重启应用再重新插入一条数据验证不要只改连接串不重建库旧数据已经按错误编码存进去就是乱码改库的字符集解决不了存量数据的问题。这个坑最迷惑的地方在于乱码可能只出现在部分表检查时先用SHOW CREATE TABLE t_material;确认每张表的字符集而不是只改整库。5.3 MySQL 8 连接错误认证插件和 SSL 是两个连续坑现象连接时报Access denied for user rootlocalhost或Public Key Retrieval is not allowed或 SSL 连接异常导致连上几秒后断开。原因MySQL 5.7 是 mysql_native_password 认证方式MySQL 8.0 默认换成 caching_sha2_password。如果使用较老的 mysql-connector-java 5.x默认只认旧认证方式握手就会失败。同时 MySQL 8 默认开启 SSL驱动第一次连接时会尝试获取服务器公钥如果没有授权或网络策略限制就报 Public Key Retrieval is not allowed。解决回答这个问题先分两步走。第一步把驱动升级到 8.0.x并确认驱动类是com.mysql.cj.jdbc.Driver而不再是旧的com.mysql.jdbc.Driver第二步在 JDBC URL 里补两个参数allowPublicKeyRetrievaltrueuseSSLfalse。如果还在用 MySQL 5.7.44 这类版本驱动仍然建议用 8.0.x它向下兼容 5.7 协议。如果老师要求你自己装数据库这几乎是每个第一次装 MySQL 8.0 的人必经的连续坑装完连不上、连上又断、断完发现是 SSL 握手慢。5.4 库存对不上与重复入库缺事务的手工翻车现场现象同一张入库单连续点击两次提交明细变成双份库存翻了倍或者出库时明明库存够操作后库存变成负数再或者程序运行中抛异常后单据表里有主单但没有明细。原因这三个现象指向同一个病根——没有使用事务。网上很多流传的课程设计源码里连接上直接执行 SQL每条语句自动提交。入库主表插入了明细插入时报错主表数据已经提交了无法撤回。重复点击是因为表单没有在第一次提交后清空或禁用按钮第二次提交把同样的数据又走了一遍完整流程。库存变成负数更是没有任何约束兜底。解决所有写操作都按第 4 章的模板走事务setAutoCommit(false)、业务 SQL、commit()、异常rollback()。界面上做防重复提交成功后清空表单并禁用提交按钮直到界面重置。库存减成负数的防线在 UPDATE 的 WHERE 条件里补stock_qty ?而不是在 Java 里先查后判。排错时如果发现库存已经不对不要试图用 UPDATE 脚本修几个数了事先翻历史单据做一次库存台账重算把当前库存调整为“入库总和 - 出库总和”再用事务机制跑一次重新核对。6. 让项目更耐看的三个细节文档组织、异常提示与导出一套能拿高分的物资信息管理系统功能做到增删改查只是及格线真正拉开差距的是那些老师一眼就能注意到的细节。第一个细节是操作结果提示。Swing 项目最常见的实现是 everywhere 直接弹JOptionPane.showMessageDialog但更专业的做法是定义一个统一的ResultMessage工具类把成功、失败、确认三类弹窗封装好。比如入库成功后提示“入库单 xxx 登记成功”失败时不只是显示异常信息而是显示“失败原因库存不足/连接中断”。这个小改动成本极低但答辩演示时每次操作都有明确反馈体验完全不同。第二个细节是导出能力。物资管理系统最常被问到的扩展需求是“把当前库存导出成 Excel”很多人一听就引入 POI 依赖结果项目体积和复杂度都翻倍。其实课程设计阶段做 CSV 导出完全够用CSV 本质是逗号分隔的文本能被 Excel 直接打开。用 JFileChooser 选择保存路径然后拼接 UTF-8 BOM 头 逗号分隔的字符串写入文件五行代码就能实现。这个方法不引入外部库也绕开了 POI 版本兼容问题适合演示“库存台账导出”这个加分项。第三个细节是文档目录的组织方式。源码配套的文档说明一般至少包含四部分需求说明、数据库设计、系统设计、测试报告其中数据库设计要给出 ER 图和表结构说明测试报告要列出测试步骤与预期结果。把文档按这四个目录分开答辩时老师要找什么都能快速翻到比大一统的单个 Word 文件可靠得多。我做课设那年的教训是代码写得再完整文档结构混乱被老师翻了半天找不到测试案例最后还是被扣了分。后来我把文档目录改成固定的四段式再把每一次运行验证的截图放进测试报告后续所有课程设计和毕业设计都沿用这套组织方式再也没有在材料上丢过分。如果你想把这套 JavaSwingMySQL 的物资信息管理系统作为自己的课设骨架建议先照着本文把建库、连接、主窗口和一条入库业务完整跑通再向外扩展其他功能。做完之后你会发现Swing 虽然是老技术但它逼着你把 JDBC、事务、SQL 这些数据库基础全都过了一遍——这些才是任何技术栈里都不会过时的东西。希望帮到你。本文还有配套的精品资源点击获取
返回列表