ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java Swing进销存系统源码解析:从登录到库存事务的完整实践

Java Swing进销存系统源码解析:从登录到库存事务的完整实践 简介这是一份面向高校课程设计与毕业设计场景的Java Swing进销存管理系统源码适合需要学习传统桌面端管理软件开发流程的学生或开发者围绕企业库存、销售、进货三大业务构建了完整的数据管理与分析功能。压缩包共325个文件其中75个Java源文件适合阅读核心业务逻辑169个class文件为编译产物56张PNG图片展示系统界面7份Word文档为设计说明另含SQL Server数据库文件MDF/LDF、JAR依赖包等整体大小约4.47MB按功能模块组织便于对照学习。目前已有599人学习下载。资源附带MyEclipse工程配置和SQL Server2000数据库文件导入后即可运行。系统涵盖信息管理、业务管理、库存管理、查询统计、系统管理五大功能模块具体包括客户、商品、供应商管理进货单与销售单处理库存盘点以及客户、商品、供应商、销售、入库等多维度查询统计类设计覆盖系统主窗体、单据操作、数据访问与查询等核心逻辑既能辅助理解进销存业务流程也可作为课程设计、毕业设计或Java实训项目的直接参考。1. 为什么还有人用 Java Swing 写进销存这套源码到底能干什么“Java Swing进销存管理系统源码”这个标题几乎每年都会在 Java 课程设计案例源码的搜索里排进前列。它面向的是一类非常具体的需求用最传统的 Java 桌面技术把采购、销售、库存、统计这一条进销存业务链路完整跑通。这套源码的价值不在界面多华丽而在业务闭环——登录、商品管理、入库出库、库存查询、报表统计每一环都能对应到数据库里的表和代码里的类。适合三类人要交课程设计作业的学生、接手公司内部老桌面系统的开发者、以及想用最小依赖理解 Java 业务代码如何组织的初学者。它不搞微服务不搞中间件但正因为技术选型传统反而最容易读懂、最容易改。2. 拿到源码包先做三件事目录、SQL脚本和入口类的定位2.1 别急着点运行先分清 src、sql、docs 三个目录很多同学拿到压缩包第一反应是解压、导入 IDE、点运行结果要么报数据库连接失败要么找不到主类。我一般建议先花十分钟把目录结构摸清楚。一个规范的 Swing 进销存项目通常长这样erp-system/ ├── src/ # Java 源码目录 │ ├── com/erp/ │ │ ├── view/ # 界面类登录、主窗体、各业务面板 │ │ ├── dao/ # 数据库访问对象 │ │ ├── model/ # 实体类Goods、Supplier、StockLog 等 │ │ ├── util/ # 数据库连接、MD5、导出等工具 │ │ └── Main.java # 入口类带 main 方法 ├── sql/ # 数据库初始化脚本 │ └── erp.sql # 建库、建表、插入初始数据 ├── docs/ # 设计文档、数据库说明、使用手册 └── lib/ # 第三方 jar 包比如 MySQL 驱动先看 docs 里的数据库设计说明再看 sql 目录下的脚本最后才看代码这是最省时间的阅读顺序。如果压缩包里没有显式分目录那就按包名去定位com.erp.view是界面层com.erp.dao是数据访问层这种分层约定在九成课设项目里都成立。找不到 lib 也不用慌看代码里import了哪些第三方类缺什么去对应官网下载即可最常见的就是 MySQL 的 JDBC 驱动。2.2 入口类在哪儿带 main 的类就是起跑线Swing 程序入口和 Web 项目不一样它不需要 Tomcat不需要容器就是一个普通的main方法。入口类通常叫Main.java、App.java或者LoginFrame.java内部带 main。打开源码文件后按Ctrl F12搜public static void main就能定位到程序起点。常见做法是入口类只做两件事初始化一个登录窗口然后把它显示出来。至于登录之后跳转到主窗口、加载哪个业务模块都在界面代码里通过事件去驱动。这样设计的用意是让登录框成为整个系统的安全闸门进销存这种带库存数据和成本信息的系统直接进主界面是不可接受的。另外注意一点如果你的 IDE 搜索 main 方法搜出来多个优先选类名带Login、Main、App的带Test结尾的基本是单元测试入口跑错了只会看到控制台输出而不是界面。2.3 建库建表进销存最少需要六张表数据库脚本是整个系统的地基。进销存系统再简化也绕不开这几张表用户表谁在操作、商品表卖什么、库存表还剩多少、进货单进来了什么、销售单出去了什么、库存流水每一步变化的痕迹。下面这段建表语句是典型设计你拿到手可以直接执行CREATE DATABASE IF NOT EXISTS erp DEFAULT CHARSET utf8mb4; USE erp; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5值, real_name VARCHAR(32) COMMENT 姓名, role VARCHAR(16) DEFAULT OPERATOR COMMENT ADMIN/OPERATOR, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 ); CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL UNIQUE COMMENT 商品编码, name VARCHAR(64) NOT NULL COMMENT 商品名称, spec VARCHAR(64) COMMENT 规格型号, unit VARCHAR(16) COMMENT 单位, price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 参考售价, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, min_stock INT DEFAULT 0 COMMENT 库存下限预警, status TINYINT DEFAULT 1 ); CREATE TABLE stock_log ( id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL, type TINYINT NOT NULL COMMENT 1入库 2出库 3盘点调整, quantity INT NOT NULL, before_stock INT NOT NULL, after_stock INT NOT NULL, biz_no VARCHAR(32) COMMENT 关联业务单号, operate_user VARCHAR(32), operate_time DATETIME DEFAULT CURRENT_TIMESTAMP );utf8mb4是必须的不然商品名称里有生僻字或者 emoji 会直接报错DECIMAL而不是float存金额这是财务数据的基本底线stock_log表记录每次操作前后的库存快照是排查库存差异和做审计的重要依据。原项目如果只有五张表通常是把进货单和销售单合并成了一张biz_order表用type字段区分类型也能跑通但你后续写报表会麻烦一些。3. 登录到主窗口Swing 事件机制和分层是怎么配合的3.1 数据库连接工具类直连好还是连接池好Swing 进销存程序里最常见的数据库连接做法是 JDBC 直连原因很简单这类系统通常是小规模局域网内使用并发量低C3P0、Druid 这些连接池在这里属于杀鸡用牛刀。但直连也有讲究不能每个按钮事件里都写一遍DriverManager.getConnection那样代码会散落一地。一个规范的工具类长这样public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/erp ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PWD 123456; static { try { 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, PWD); } }注意几个参数characterEncodingutf8是为了让中文正常读写缺失它你会在查询时看到一堆问号useSSLfalse是关闭 MySQL 8.x 默认的 SSL 校验不写会多一段握手警告serverTimezone指定时区MySQL 8 没它会报时区错误。密码直接写在代码里确实不安全但课设和内部系统这么干的占绝大多数如果你要做更正规的交付可以把连接信息放到config.properties文件里代码用Properties类去读取这样换数据库环境不用改代码重新编译。3.2 登录验证的代码骨架校验、加盐和会话登录按钮是进销存系统的第一个事件入口。新手常犯的错误是校验逻辑写得太薄——文本框非空就放行这是把界面校验当成了安全认证。一个合格的登录代码要实现三层输入校验、凭证比对、会话记录。下面这段是骨架注意它是写在登录窗体的按钮事件里的btnLogin.addActionListener(e - { String username txtUser.getText().trim(); String password new String(txtPwd.getPassword()); // 第一层输入校验 if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空); return; } // 第二层数据库比对密码采用MD5存储 String sql SELECT id, real_name, role FROM sys_user WHERE username ? AND password ? AND status 1; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, Md5Util.md5(password)); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { // 第三层记录会话然后跳转主窗口 UserContext.setLoginUser(new User(rs.getInt(id), rs.getString(real_name), rs.getString(role))); new MainFrame().setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } } } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, 数据库连接失败请检查服务); } });这里有几个细节值得说明。第一密码框用getPassword()返回的是char[]而不是字符串这是 Swing 的设计字符数组用完后可以立刻清空减少内存中被反射拿到密码的风险所以千万别为省事改成getText()第二SQL 里的status 1是一个很容易被忽略的过滤条件它能让你在不停用账号的情况下禁止其登录第三UserContext是一个很简单的静态类用ThreadLocal或者直接静态变量保存当前登录用户主窗体里的所有业务操作都能拿到操作人姓名这是后面库存流水表operate_user字段的数据来源。3.3 主界面用 JTabbedPane 装下七个业务页登录成功之后进入主窗体大部分进销存系统的主界面结构是左侧功能菜单配右侧内容区但 Swing 项目里更常见、也更省事的布局是用JTabbedPane做顶部页签。这样做的好处是新窗口不用一个个弹登录一次后所有功能都在一个窗口内切换而且代码组织起来清晰——每个页签对应一个JPanel子类。public class MainFrame extends JFrame { public MainFrame() { setTitle(进销存管理系统 - 当前用户 UserContext.getLoginUser().getRealName()); setSize(1024, 700); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JTabbedPane tabs new JTabbedPane(); tabs.addTab(商品管理, new GoodsPanel()); tabs.addTab(入库管理, new StockInPanel()); tabs.addTab(出库管理, new StockOutPanel()); tabs.addTab(库存查询, new StockQueryPanel()); tabs.addTab(报表统计, new ReportPanel()); tabs.addTab(供应商管理, new SupplierPanel()); tabs.addTab(系统设置, new SettingsPanel()); setContentPane(tabs); } }标题栏带上当前登录用户的名字是一种很实用的操作反馈多人在一台机器上轮流操作时一眼能看出当前是谁在使用。JTabbedPane的切换事件有时间开销吗几乎没有因为它默认只加载当前页但要注意如果你的业务面板构造方法里就去查数据库那么主窗口一打开就会执行七个面板的数据库查询启动会明显变慢。解决办法是把数据加载放到面板的updateUI或者首次显示时再触发这是后面避坑章节要重点讲的性能问题。4. 库存和出入库的三个核心代码场景4.1 商品表格数据绑定AbstractTableModel 的正确用法进销存界面里最常出现的组件是表格商品列表、入库记录、出库记录全是表格。新手容易把数据一个格子一个格子塞进DefaultTableModel这样不仅代码臃肿而且商品实体改个字段就要同步改表格代码。常见的做法是自定义一个继承AbstractTableModel的模型类把你的实体List直接放进去public class GoodsTableModel extends AbstractTableModel { private String[] columns {商品编码, 名称, 规格, 单位, 售价, 库存, 状态}; private ListGoods list; public void setGoodsList(ListGoods list) { this.list list; fireTableDataChanged(); // 通知表格重新渲染 } Override public int getRowCount() { return list null ? 0 : list.size(); } Override public int getColumnCount() { return columns.length; } Override public String getColumnName(int column) { return columns[column]; } Override public Object getValueAt(int rowIndex, int columnIndex) { Goods g list.get(rowIndex); switch (columnIndex) { case 0: return g.getCode(); case 1: return g.getName(); case 2: return g.getSpec(); case 3: return g.getUnit(); case 4: return g.getPrice(); case 5: return g.getStock(); case 6: return g.getStatus() 1 ? 在售 : 停用; default: return null; } } }使用的时候查询完数据库直接把结果丢给模型GoodsTableModel model new GoodsTableModel(); JTable table new JTable(model); // 查询后 model.setGoodsList(dao.findAll());fireTableDataChanged()是本段代码的灵魂。没有这行你改了list的数据表格界面上纹丝不动看起来就像数据没查进去。为什么不直接在表格上操作因为 Swing 的表格是可重用的视图组件数据模型和界面显示解耦之后同一个模型可以对应多种展示方式打印报表时也能复用这个模型的数据。等后续要加渲染逻辑时比如库存低于下限的红色标亮在你的模型里重写getForeground或者getBackground方法就行不用动任何业务代码。4.2 出入库必须走事务库存表和流水表一个都不能少出入库是进销存里逻辑最重的操作它本质上要完成三件事记录进货单或销售单、修改goods表的库存字段、在stock_log表里留一条流水。这三件事只要其中一件失败整个操作就是半途而废。比如扣了库存但没写流水下个月对账就怎么都对不上。所以事务是必须的public boolean stockOut(StockOutDTO dto) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查库存并校验是否足够 String checkSql SELECT stock FROM goods WHERE id ?; int stock; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, dto.getGoodsId()); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) throw new RuntimeException(商品不存在); stock rs.getInt(stock); if (stock dto.getQuantity()) { throw new RuntimeException(库存不足当前仅剩 stock); } } } // 2. 更新库存 String updateSql UPDATE goods SET stock stock - ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, dto.getQuantity()); ps.setInt(2, dto.getGoodsId()); ps.executeUpdate(); } // 3. 写流水 String logSql INSERT INTO stock_log (goods_id, type, quantity, before_stock, after_stock, biz_no, operate_user) VALUES (?, 2, ?, ?, ?, ?, ?); try (PreparedStatement ps conn.prepareStatement(logSql)) { ps.setInt(1, dto.getGoodsId()); ps.setInt(2, dto.getQuantity()); ps.setInt(3, stock); ps.setInt(4, stock - dto.getQuantity()); ps.setString(5, dto.getBizNo()); ps.setString(6, UserContext.getLoginUser().getRealName()); ps.executeUpdate(); } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException(出库失败 e.getMessage(), e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); } catch (SQLException ignored) {} try { conn.close(); } catch (SQLException ignored) {} } } }这段代码要注意两点。一是setAutoCommit(false)必须在任何 SQL 执行前调用如果你先执行了一条查询再关掉自动提交那之前的操作已经独立提交了二是finally里要把autoCommit恢复为true因为连接池或下次复用时不会主动重置它。before_stock和after_stock的设计是这套方案里最有价值的细节——它让每一次库存变化的可追溯性变得非常强出了问题可以直接对比流水里的前后值快速定位到是哪个环节多扣了或少加了。4.3 条件查询拼 SQLPreparedStatement 参数化是底线进销存的查询功能有一个共同套路有一组可选的条件输入框用户填了某个条件就按某个字段过滤不填就查全部。很多人在这一步会用字符串拼接直接写 SQL这是最危险的习惯。我之前接手过一个项目查询代码里赫然写着WHERE name keyword 这种语句一旦有人输入 OR 11就能绕过全部分页逻辑这种系统对外上线就是裸奔。参数化的写法明明更简洁public ListGoods searchGoods(String name, Integer status) { StringBuilder sql new StringBuilder(SELECT * FROM goods WHERE 1 1 ); ListObject params new ArrayList(); if (name ! null !name.trim().isEmpty()) { sql.append(AND name LIKE ? ); params.add(% name.trim() %); } if (status ! null) { sql.append(AND status ? ); params.add(status); } sql.append(ORDER BY id DESC); ListGoods result new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { result.add(mapToGoods(rs)); } } return result; } catch (SQLException e) { throw new RuntimeException(查询商品失败, e); } }WHERE 1 1看着像个笨拙的技巧但它让后面每一个AND拼接都变得安全——不用判断当前是不是第一个条件不用考虑加AND还是WHERE。setObject可以省去逐个判断参数类型的麻烦它内部会根据PreparedStatement的元数据做适配。这里想强调的是模糊查询的%要和参数一起绑定而不是拼进 SQL 字符串原因依然是防止特殊字符注入。参数化这条底线守住配合LIMIT分页一个桌面系统的查询层就足够稳了。5. 进销存系统避坑指南六个常见翻车现场与解决5.1 界面假死在 EDT 上跑 JDBC卡到怀疑人生现象点击“查询”按钮后整个窗口变成白色无响应状态任务栏显示“未响应”过几秒甚至几十秒才恢复。原因Swing 的界面刷新和事件响应都在 EDTEvent Dispatch Thread线程上执行你把数据库连接、查询、遍历结果集这些耗时操作全放在按钮事件里等于在唯一能刷界面的线程上做阻塞界面自然卡死。数据量大、网络慢的时候尤其致命。解决耗时操作用SwingWorker丢到后台线程执行查询期间显示进度条或禁用按钮完成后再切回 EDT 更新界面。最常见的最小改动是btnSearch.addActionListener(e - { btnSearch.setEnabled(false); new SwingWorkerListGoods, Void() { Override protected ListGoods doInBackground() { return goodsDao.searchGoods(txtName.getText(), null); } Override protected void done() { try { model.setGoodsList(get()); btnSearch.setEnabled(true); } catch (Exception ex) { JOptionPane.showMessageDialog(null, 查询失败 ex.getMessage()); } } }.execute(); });UI 操作一律只在done()里做别在doInBackground()里碰任何组件。记住一句话数据库可以慢界面永远不能等。5.2 中文乱码编码不一致的连锁反应现象登录界面中文正常查询出来的商品名称是????或者æžœæ±这类乱码。原因乱码问题通常是三个环节里至少有一个编码不一致——Java 源文件编译编码IDE 保存编码、JDBC 连接串的字符集、数据库表的默认字符集。常见组合是源码保存为 UTF-8连接串没指定字符集数据库表建的是 latin1三处对不上自然乱码。解决把三个环节统一为一种编码推荐全程 UTF-8。IDE 里把项目编码、文件编码全部设为 UTF-8数据库连接串加上characterEncodingutf8建库时显式指定默认字符集建好的表可以用ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4修正。改完后重启数据库连接如果还是乱码检查原库里的数据是不是已经损坏是的话只能重新导入 SQL 脚本。5.3 表格不刷新忘了 fireTableDataChanged现象入库操作成功后切回商品管理页库存数字还是旧的必须重新打开窗口才能看到更新。原因数据源更新了但表格模型没有通知视图重绘。JTable不会自动感知List里的变化它完全依赖模型层的通知机制。用的是自定义AbstractTableModel就必须在数据变更后手动调用fireTableDataChanged()或者更细粒度的fireTableRowsUpdated()。解决在GoodsTableModel.setGoodsList()末尾加fireTableDataChanged()这是治本的改法所有调用它更新数据的地方都会自动刷新。如果项目用的是DefaultTableModel更新数据时要调用setDataVector()或setRowCount(0)再重新addRow()完事同样要记得触发。5.4 金额计算翻车double 精度问题现象进货 3 件商品单价 19.9 元总价算出 59.699999999999996打印出来用户看着想砸电脑。原因double是二进制浮点数无法精确表示 19.9、0.1 这种十进制小数这是硬伤跟算法无关。解决金额字段类型改成BigDecimal数据库字段用DECIMAL(10,2)匹配。计算时统一用BigDecimal的add、multiply、subtract方法构造时用字符串而不是 doublenew BigDecimal(19.9)。如果你是从ResultSet里拿getBigDecimal(price)那本身就是BigDecimal直接用即可。千万别图省事在BigDecimal和double之间来回转转一次精度就丢一次。5.5 点关闭窗口进程不退出隐藏的 AWT 线程现象关闭登录窗或主窗口后程序窗口消失了但 IDE 控制台里的红色方块还在说明 JVM 没退出。原因setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)没有生效或者代码里手动创建的线程定时备份、后台任务没有停止。尤其是如果登录后只dispose()了登录窗口而程序的主入口线程还在等待JVM 就不会退出。解决入口类的main方法里对主窗口设置EXIT_ON_CLOSE并在WindowClosing事件里加打印日志确认关闭事件确实被触发。如果你写了java.util.Timer做定时任务记得关闭时调用cancel()否则那个 timer 线程会永远挂住 JVM。5.6 设计文档和代码对不上文档是上上版现象按文档里的数据库说明建表导入代码后运行报字段不存在。原因课设项目常见通病文档写于开发前一版或开发过程中改了设计但忘了同步文档代码才是活的文档是死的。解决以源码为准doc 为参考。跑不起来先看 sql 目录下的脚本是不是最新、有没有被二次修改过如果 sql 脚本按文档建表但查询代码里引用了文档里没有的字段说明代码和脚本脱节了优先改数据库脚本适配代码。真正做交付时建议反过来——进销存这类系统文档里的数据库设计应该是根代码跟着文档走。如果是别人写的代码你只是接手维护那只能先信代码再把文档修正到跟代码一致别省这一步不然下个人继续踩这个坑。6. 交付前最后一步打 jar 包、写启动脚本、留一条日志后路进销存系统写到能跑只是第一步能交付给使用者双击运行才是完整的闭环。开发环境里点 IDE 绿色按钮运行程序别人电脑上没有你的 JDK 配置可能根本启动不了。常见做法是打一个可执行 jar配合一个.bat或.vbs启动脚本把环境配置细节全部封装掉。# 编译产物在 out/ 目录下lib 里有 mysql-connector jar cfm erp.jar MANIFEST.MF -C out . -C lib .MANIFEST.MF里声明主类和类路径Main-Class: com.erp.Main Class-Path: lib/mysql-connector-j-8.0.33.jar启动脚本可以用最直接的start.batecho off javaw -Xms256m -Xmx512m -jar erp.jar用javaw而不是java是为了不弹出黑色控制台窗口。如果使用者机器上没配过 PATH脚本里显式写死 JDK 路径更稳妥C:\Java\jdk-17\bin\javaw.exe。交付前一定要在没装 IDE 的机器或虚拟机里实测一次这一步能暴露大量环境问题。另外一个容易被忽略的点是日志——写一个简单的LogThread在关闭时把当天的操作记录到本地logs/目录排障和交接时这是后悔药级别的好习惯。进销存这个方向真正练人的地方不在用没用过高级框架而在事务、状态、边界条件这些基本功。我见过太多面试时能背 java 八股文的人一说库存扣减和流水一致性就露馅。这套 Swing 进销存源码你把它从头到尾改一遍、跑一遍、再亲手部署给模拟用户用一次比看十篇架构文章都实在。希望帮到你。本文还有配套的精品资源点击获取
返回列表