简介:Java Swing进销存管理系统是一套面向Java初学者与课程设计人群的完整源码包,围绕企业库存、销售、进货三大核心业务,提供信息管理、业务管理、库存盘点、查询统计与系统操作等模块,可帮助读者理解Swing界面开发与SQL Server 2000数据库的整合应用。压缩包共325个文件,大小仅4.47MB,其中75个java源文件与169个class文件构成可运行主体,doc文档用于说明设计与使用,png/gif图片辅助界面展示,jar包提供依赖库,ldf/mdf为SQL Server数据库文件。从内容预览可以观察到系统主窗体、数据访问对象与业务查询类等典型实现结构,便于按业务模块对照学习。目前已有600人学习浏览,适合需要完成课程设计、毕业设计或快速上手Java桌面进销存项目的读者,下载后即可获得源码、数据库及相关设计文档,省去从零搭建环境的时间。
1. Java Swing 进销存管理系统源码还值得看吗:课程设计与小型商户的实际选型
如果你正在找 Java Swing 进销存管理系统源码,多半是课程设计或毕业设计卡住了,要么就是想给熟人小店做一套简单的进出货登记工具。这套源码的界面用 Swing 做,数据库是 sqlserver2000,技术栈看起来老,业务闭环却一点不缺:客户、商品、供应商三套基础资料,进货单、销售单两份业务单据,外加库存盘点、查询统计、系统管理,数据库文件和设计文档也打包在压缩包里。
我把它拆出来的价值在于:它没有 Spring Boot 那种厚重依赖,类与类之间靠 JDBC 和 DAO 直接串起来,能让你一次性看明白“窗口怎么调 DAO、DAO 怎么操作 SQL Server”。它适合两类人:一类是 Java 课程设计还课没交的,另一类是打算把进销存改造成小店内部工具、想先有一份完整骨架的从业者。
2. 从 class 反推整套进销存设计:先读懂业务边界,再动代码
2.1 一个 class 文件清单,就是一张分层架构图
压缩包里的 SystemMain.class、SaleGoods.class、InGoods.class、GoodsInfo.class、GuestInfo.class、SeverInfo.class、JSalesQuery.class、GoodsInfoDao.class、InsertGoodsInfo.class、SaleDao.class,其实已经把项目分层写在了文件名上。SystemMain 是程序入口;GoodsInfo、GuestInfo、SeverInfo 分别是商品、客户、供应商三个实体;InGoods、SaleGoods 是进货和销售两张业务单据;JSalesQuery 是查询统计;GoodsInfoDao、SaleDao、InsertGoodsInfo 负责数据库操作。我习惯把这些类名先摊开,再对照功能模块,比直接打开所有源文件一个个点要快得多。
| class 名 | 推断职责 | 对应功能 |
|---|---|---|
| SystemMain | 启动入口,负责拉起主窗口 | 系统启动 |
| GoodsInfo / GuestInfo / SeverInfo | 商品、客户、供应商实体 | 信息管理 |
| InGoods / SaleGoods | 进货单、销售单窗口与业务 | 业务管理 |
| GoodsInfoDao / SaleDao | 商品、销售相关的 JDBC 操作 | 数据访问层 |
| InsertGoodsInfo | 新增商品时的写入辅助 | 信息管理 |
| JSalesQuery | 销售查询、入库查询 | 查询统计 |
老项目的类命名经常不统一,像 SeverInfo 就看不出是供应商还是服务器,我倾向于按“供应商”理解,因为功能模块里明确有供应商管理。这块不需要 100% 精确,先把它当成一张对照表,后续反推数据库表会更顺。
// 根据 class 清单还原的调用结构,注意这不是反编译原码 public class SystemMain { public static void main(String[] args) { // Swing 组件创建必须在事件分发线程中完成 java.awt.EventQueue.invokeLater(() -> new MainFrame().setVisible(true)); } } public class GoodsInfoDao { // 老项目典型 DAO:连接、查询、增删改都在这一个类里 public List<GoodsInfo> findAll() { /* 执行 SELECT * FROM goods_info */ } public int insert(GoodsInfo info) { /* 执行参数化 INSERT */ } }这里的关键是 EventQueue.invokeLater,它把窗口创建丢给 Swing 的事件分发线程,避免在非 EDT 线程里碰组件导致随机闪退。MainFrame 只是示例名,原包可能不叫这个;重点是 SystemMain 承担入口角色。GoodsInfoDao 的存在说明作者已经把数据库操作从窗口类里拆出来,这对后面换数据库、改查询逻辑都有好处。
2.2 一张业务单据的保存,至少包含两条 SQL:单据写入与库存更新
进销存最容易踩坑的是“保存”按钮。保存进货单并不是只 INSERT 一张表,而是先写进货主表,再写进货明细,最后把商品库存加上去。销售单则反过来:写销售单,再把库存减掉。如果原作者把三个操作写成三个独立 executeUpdate,就会出现单据在但库存对不上的问题。
public boolean saveSale(SaleGoods sale) { Connection conn = getConnection(); PreparedStatement ps = null; try { conn.setAutoCommit(false); // 手动管理事务,任何一步失败整体回滚 ps = conn.prepareStatement( "INSERT INTO sale_goods(order_no, goods_id, qty, price, sale_date) VALUES(?,?,?,?,?)"); ps.setString(1, sale.getOrderNo()); ps.setInt(2, sale.getGoodsId()); ps.setInt(3, sale.getQty()); ps.setBigDecimal(4, sale.getPrice()); ps.setTimestamp(5, new Timestamp(sale.getSaleDate().getTime())); ps.executeUpdate(); // 同步扣减库存,这一步是最容易漏掉的 ps = conn.prepareStatement( "UPDATE goods_info SET stock_qty = stock_qty - ? WHERE goods_id = ?"); ps.setInt(1, sale.getQty()); ps.setInt(2, sale.getGoodsId()); ps.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn != null) conn.rollback(); return false; } finally { // 实际代码要关闭 rs/ps/conn,这里省略 } }setAutoCommit(false) 是这段代码的生命线。SQL Server 2000 默认自动提交,不关掉它,commit 和 rollback 都不生效。order_no 是业务流水号,进入方法前就要生成好,常见规则是日期加序号;qty 用 int 传参,price 用 BigDecimal,避免金额精度损失。拿到源码后,先搜保存按钮的 ActionListener,看有没有 commit 和 rollback,就能判断原作者有没有把事务做完整。
2.3 由功能清单倒推数据库表:SQL Server 2000 建表语法与 MySQL 的差异
摘要里的功能模块正好对应几张表:客户管理、商品管理、供应商管理对应三个基础表;进货、销售对应单据表;库存盘点需要一个能存账面数量与实际数量的库存表。sqlserver2000 建表和 MySQL 差异很大,最典型的三条:自增字段用 IDENTITY(1,1) 而不是 AUTO_INCREMENT;中文要用 NVARCHAR;金额不要用 FLOAT。
CREATE TABLE goods_info ( goods_id INT IDENTITY(1,1) PRIMARY KEY, goods_name NVARCHAR(50) NOT NULL, spec NVARCHAR(50), unit NVARCHAR(10), price DECIMAL(18,2), stock_qty INT DEFAULT 0 ); CREATE TABLE in_goods ( order_no VARCHAR(20) PRIMARY KEY, goods_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(18,2), in_date DATETIME DEFAULT GETDATE() );IDENTITY(1,1) 表示从 1 开始、每次加 1,是 SQL Server 2000 的标准自增写法。DATETIME 配合 DEFAULT GETDATE(),省得每次手动传系统时间。DECIMAL(18,2) 是金额字段的通用定义,整数位 16 位、小数位 2 位,小商户的进销存完全够用。如果压缩包里有现成的 .sql 文件,优先以原文件为准,上面这段只是辅助理解字段含义。
2.4 查询统计模块不是花架子:销售查询该看哪张表
功能清单里的“销售查询、入库查询”往往被当成附属功能,实际反而是最容易被问到的。很多初学者以为查询就是 SELECT * FROM sale_goods,再把结果摆到 JTable 里。但真正的销售报表必须有商品维度汇总,否则回答不了“这个月哪个商品卖得最多”这种基本问题。
SELECT g.goods_name, SUM(s.quantity) AS total_qty, SUM(s.quantity * s.price) AS total_amount FROM sale_goods s LEFT JOIN goods_info g ON s.goods_id = g.goods_id WHERE s.sale_date BETWEEN ? AND ? GROUP BY g.goods_name ORDER BY total_amount DESC;BETWEEN ? AND ? 接收 Swing 界面上两个日期输入框的参数,建议先用 SimpleDateFormat 转成 java.util.Date,再用 setTimestamp 设参,不要直接拼字符串。LEFT JOIN 保留商品资料被删掉的历史销售记录,避免汇总行莫名其妙消失。如果原工程用的是s.sale_date >= ? AND s.sale_date <= ?,效果等价,只是可读性不同。
3. 让老项目跑起来:MyEclipse 导入与 SQL Server 2000 的握手细节
3.1 环境年代匹配:JDK、IDE、数据库怎么选
老项目和新工具之间的兼容性,是卡住大部分人的第一道门槛。这套源码写在 MyEclipse 时代,数据库是 sqlserver2000,你用 MySQL 8 或者 SQL Server 2019 直接跑原包,大概率会挂在驱动上。我的建议是:先解压压缩包看设计文档,确认里面是 .bak 备份文件还是 .sql 脚本,再用对应年代的数据库环境去还原,不要硬拿新库硬套。
| 组件 | 建议配置 | 说明 |
|---|---|---|
| JDK | JDK 1.6 或 1.8 | 编译和运行 Swing 程序,老驱动在 1.6 下更省心 |
| IDE | MyEclipse 或 Eclipse JEE | 老工程直接导入比较方便 |
| 数据库 | SQL Server 2000 | 还原 .bak 或执行 .sql |
| JDBC 驱动 | Microsoft 三件套或 jTDS 单 jar | Java 连接 sqlserver2000 的桥梁 |
如果你只有 JDK 17 也别急着放弃,可以尝试运行,但要有心理准备:微软 2000 的 JDBC 驱动在 JDK 9 之后可能因为模块化机制加载不出来,此时换成 jTDS 驱动能救回大半。我一般会在第一步就把驱动选择定下来,不要等项目报了异常再去翻 jar。
3.2 导入工程:把源码放进 MyEclipse
导入这套源码不算复杂,但有几个细节容易翻车。先说明常规步骤:
- 解压压缩包,找到 src 目录和 lib 目录,阅读设计文档确认数据库脚本位置。
- 打开 MyEclipse,File -> New -> Java Project,工程名随意,比如 jxc。
- 如果解压目录里已有 .classpath 和 .project,直接 Import -> Existing Projects into Workspace;如果没有,就新建工程再把源码复制进 src。
- 右键项目 -> Properties -> Java Build Path -> Libraries -> Add JARs,把 lib 下的驱动 jar 全部加进来。
- 找到 SystemMain.java,右键 Run As -> Java Application。
# 导入前先确认 driver jar 存在且完整 ls -l msbase.jar mssqlserver.jar msutil.jar # 确认驱动类确实在 jar 里;Windows cmd 把 grep 换成 findstr /i jar tf mssqlserver.jar | grep -i SQLServerDriverjar tf 是查看 jar 包内容的 JDK 自带命令。如果搜不到 com/microsoft/jdbc/sqlserver/SQLServerDriver.class,说明 jar 放错或版本不匹配。导入时如果项目名出现红叉,先检查 Build Path,再看工程编码,老项目常用 GBK,如果你用 UTF-8 打开源码,中文会乱成一团,项目级编码设置要统一。
3.3 还原数据库并修改连接串:让 sa 账号真正打通
SQL Server 2000 的还原有两条路:一是企业管理器右键数据库 -> 所有任务 -> 还原数据库,选备份文件;二是直接执行 SQL 命令。命令行方式更适合反复建环境。
-- 还原进销存数据库,库名以压缩包文档为准,下面只是常见写法 RESTORE DATABASE jxc FROM DISK = 'D:\backup\jxc_db.bak' WITH REPLACE, MOVE 'jxc_Data' TO 'D:\Program Files\Microsoft SQL Server\MSSQL\Data\jxc.mdf', MOVE 'jxc_Log' TO 'D:\Program Files\Microsoft SQL Server\MSSQL\Data\jxc_log.ldf';MOVE 后面的逻辑文件名不能瞎编,企业管理器还原界面会显示备份里的原始逻辑名。如果目标目录不存在,SQL Server 2000 会报 5118 错误。还原完成后,打开源码里的 DAO 类,改连接字符串。
// Microsoft 驱动的经典写法 private String url = "jdbc:microsoft:sqlserver://127.0.0.1:1433;DatabaseName=jxc"; private String user = "sa"; private String password = "123456"; // 换成 jTDS 驱动则是 // private String url = "jdbc:jtds:sqlserver://127.0.0.1:1433/jxc";这里的库名 jxc 是示例,以实际还原出的库名为准。url 里的 microsoft 段是微软驱动专属标记,jTDS 的写法完全不同。另一个易错点是 localhost,老驱动对 IPv6 解析可能有兼容问题,连接失败时优先把 localhost 改成 127.0.0.1 试一次。
3.4 跑通后的链路验证:从商品到进货单到库存
环境搭好不是用来展示“能启动”的,而是为了证明关键业务路径是通的。我的验证顺序是登录 -> 新增商品 -> 做进货单 -> 查库存。这条链路覆盖了基础资料、业务单据和库存三块核心代码,任何一步卡住都说明对应模块有问题。
- 运行 SystemMain,用连接串里的 sa 账号密码登录。
- 进入商品管理,新增一个测试商品,例如“测试圆珠笔”,保存。
- 进入进货管理,选择该商品,数量填 50,保存进货单。
- 进入库存盘点,看该商品库存是否变成 50。
-- 手工核对库存,商品名换成你新增的那条 SELECT goods_name, stock_qty FROM goods_info WHERE goods_name = '测试圆珠笔';如果商品能查到但库存没变,问题几乎都出在进货保存逻辑,常见的是只写了进货表、没有更新 goods_info。回到上一章的事务思路,先看保存按钮里有没有库存更新语句,再看有没有 commit。
4. 避坑指南:SQL Server 2000 连接与 Swing 运行的四个高频翻车点
4.1 驱动加载失败:ClassNotFoundException 不是玄学,是 jar 没进去
现象:运行 SystemMain 后抛出 java.lang.ClassNotFoundException: com.microsoft.jdbc.sqlserver.SQLServerDriver,或者类加载到了但执行 getConnection 时又报 NoClassDefFoundError。
原因:驱动 jar 没有加入 Build Path。MyEclipse 里最常见的坑是:lib 下的 jar 文件明明在,但你只是复制到了项目目录,没有右键点 Add JARs,运行时类路径里根本找不到它。
解决:打开项目属性,Java Build Path -> Libraries -> Add JARs,把 msbase.jar、mssqlserver.jar、msutil.jar 一次性加进去;或者换 jTDS 的 jtds.jar,一个文件搞定。加完执行 Project -> Clean 再启动,避免旧的 class 缓存没被刷新。
// 在 DAO 类里显式加载驱动,便于第一时间定位问题 static { try { Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver"); } catch (ClassNotFoundException e) { // 抛 RuntimeException,控制台会直接告诉你缺哪个类 throw new RuntimeException("SQL Server 2000 JDBC 驱动不存在,检查 lib 目录", e); } }Class.forName 的作用是把驱动类注册到 DriverManager。如果原工程已经写了就不用动,这段只是辅助排查。jTDS 驱动对应的类名是 net.sourceforge.jtds.jdbc.Driver,两个驱动不能混用。
4.2 端口连不上:SQL Server 2000 默认没开放 TCP/IP
现象:getConnection 卡很久后报 java.net.ConnectException: Connection refused,或者提示无法连接到 1433 端口。
原因:SQL Server 2000 默认安装时可能只启用命名管道,TCP/IP 协议没打开;或者 Windows 防火墙拦了 1433。还有一种次常见原因是 localhost 被解析成 IPv6 地址,老驱动不认。
解决:到开始菜单打开“服务器网络实用工具”,在启用协议里勾上 TCP/IP,重启 SQL Server 服务;防火墙放行 1433;Java 连接串把 localhost 改成 127.0.0.1。
# 检查 1433 端口是否在监听,Windows cmd 执行 netstat -an | findstr 1433看到0.0.0.0:1433 LISTENING才算数据库在正常监听。如果只有127.0.0.1:1433,说明实例只对本机开放,其他机器连接时要做额外网络配置。如果什么都没输出,就是 TCP/IP 还没启用。
4.3 中文乱码:NVARCHAR 和驱动编码对不齐
现象:在 Swing 界面输入“洗发水”保存,再查出来变成“????”;或者界面看着正常,数据库里已经是乱码。
原因:建表用了 VARCHAR 而不是 NVARCHAR。SQL Server 2000 的 VARCHAR 在简体中文环境下用 GBK 存,JDBC 驱动默认按 Unicode 传,两边编码规则不一致,汉字就被写坏。另一个常见原因是数据库排序规则不是中文排序。
解决:建表字段全部用 NVARCHAR;数据库排序规则设置为 Chinese_PRC_CI_AS。如果数据已经乱掉,只能先修数据再改表结构。老代码里有一种应急转码写法,可以作为参考,但不建议抄到新代码里。
// 老项目遗留的应急转码:把读出来的错误编码转回 GBK String goodsName = rs.getString("goods_name"); goodsName = new String(goodsName.getBytes("ISO-8859-1"), "GBK");这段代码只在“驱动把 Unicode 字节错解释成 ISO-8859-1”的场景有效,不同错误模型下转了也白转。我一般先用 SQL Server 查询工具直接看数据,确定乱码发生在写入阶段还是读取阶段,再决定修库还是修代码,不要一上来就套转码。
4.4 库存对不上:保存逻辑缺事务,重复点击又叠加扣减
现象:做了三笔进货单,库存盘点总数和进货流水对不上;或者一张销售单保存了五次,库存被扣了五倍。
原因:老代码常见两种问题:一是每条 SQL 独立自动提交,中间某条失败没有回滚;二是保存按钮的监听没有做防重复处理,用户双击按钮,库存更新执行了两次。
解决:保存方法用 conn.setAutoCommit(false) 包住所有语句;按钮点击后立即 setEnabled(false),成功后再恢复。排查阶段先查单据有没有重复流水号,再核对库存。
-- 检查同一个销售单号是否被重复插入 SELECT order_no, COUNT(*) AS cnt FROM sale_goods GROUP BY order_no HAVING COUNT(*) > 1;这条 SQL 能直接把重复插入的流水号暴露出来。如果结果非空,说明保存按钮确实被重复执行。之后可以给订单号加唯一索引,数据库层面兜底,比单纯禁用按钮更可靠。
-- 核对库存公式:账面库存 = 累计进货 - 累计销售 SELECT g.goods_id, g.goods_name, g.stock_qty, ISNULL(i.in_qty, 0) - ISNULL(s.sale_qty, 0) AS calc_qty FROM goods_info g LEFT JOIN (SELECT goods_id, SUM(quantity) AS in_qty FROM in_goods GROUP BY goods_id) i ON g.goods_id = i.goods_id LEFT JOIN (SELECT goods_id, SUM(quantity) AS sale_qty FROM sale_goods GROUP BY goods_id) s ON g.goods_id = s.goods_id WHERE g.stock_qty <> ISNULL(i.in_qty, 0) - ISNULL(s.sale_qty, 0);ISNULL 是 SQL Server 2000 支持的函数,用于把 NULL 转成 0,避免减法结果变成 NULL。如果这条核对 SQL 查出数据,说明单据和库存不同步,直接回源码查保存按钮里的事务处理。
5. 把库存盘点做成实用功能:差异预览与 CSV 导出
5.1 盘点界面的三个字段缺一不可
很多 Swing 的库存盘点界面,只是把 goods_info 表的 stock_qty 显示出来,没有给用户填实盘数量的入口,严格说这不是盘点,只是看库存。我的做法是在 JTable 旁边加一个“实盘数量”输入框,程序实时算差额,把差异行标红。数量通常是整数,直接用 int 计算就够了。
int stockQty = Integer.parseInt(model.getValueAt(row, 0).toString()); String input = checkField.getText().trim(); if (input.length() == 0) return; // 输入为空时跳过,避免 NumberFormatException int checkQty = Integer.parseInt(input); int diff = stockQty - checkQty; model.setValueAt(diff, row, 3); // 第 3 列专门放差异数这里第 0 列是账面数,第 3 列是差异数,实际写代码要按自己的表格列顺序调整。parseInt 一定要包 try-catch,用户输入非数字字符时整个窗口崩掉,这种低级错误最容易在验收时被当场抓包。
5.2 CSV 导出的编码坑:GBK 比 UTF-8 更稳
盘点结果最终要导出对账,最轻量的方案是 CSV。CSV 的坑主要在编码:用 UTF-8 写出来的文件,Excel 打开经常乱码;用 GBK 反而稳定。于是我把导出方法里的编码固定成 GBK,并顺手做空值兜底。
public void exportStockCsv(JTable table, File target) throws IOException { try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(target), "GBK"))) { // 表头 for (int c = 0; c < table.getColumnCount(); c++) { out.write(table.getColumnName(c)); out.write(c == table.getColumnCount() - 1 ? "\n" : ","); } // 数据行 for (int r = 0; r < table.getRowCount(); r++) { for (int c = 0; c < table.getColumnCount(); c++) { Object val = table.getValueAt(r, c); out.write(val == null ? "" : val.toString()); out.write(c == table.getColumnCount() - 1 ? "\n" : ","); } } } }OutputStreamWriter 指定 GBK,是为了让 Excel 打开 CSV 时不用手动选编码。字段内容如果带逗号、换行或引号,要提前替换成空格,否则 CSV 会被 Excel 拆成多列。这套导出的模板可以推广到销售查询、进货明细,凡是 Swing 里的 JTable 都能套用。
我第一次给别人做进销存盘点导出时,只导了界面上的账面数,没带实盘数和差异数,结果仓库对完账还要人肉补两列,差点把客户惹毛。从那以后,我只要给进销存加导出功能,就强制走一遍“三列齐全 + GBK 编码 + 空值兜底”的流程,再也没在导出这个环节翻过车。希望帮到你。
本文还有配套的精品资源,点击获取