资讯动态

Java Swing进销存管理系统源码解析:从项目结构到超卖控制实战

发布时间:2026/9/13 16:58:37 来源:尧图企业网站定制
简介这是一份基于Java Swing与SQL Server 2000的进销存管理系统完整源码包面向需要学习桌面管理软件开发或完成课程设计的开发者。系统覆盖客户、商品、供应商信息管理进货、销售业务处理库存盘点以及多维查询统计与操作权限管理可帮助企业或学习者快速掌握进销存业务的核心流程与实现方式。压缩包内共325个文件包括75个Java源文件、169个编译后的class文件、56个界面设计png图片以及doc设计文档、jar依赖库、数据库mdf/ldf文件等整体大小仅4.47MB结构清晰便于直接导入MyEclipse进行二次开发。资源还附带相关设计文档和数据库文件可辅助理解表结构与业务逻辑。目前已有599人学习下载适合初中级Java开发者作为实战参考或毕业设计基础。1. Java Swing进销存管理系统源码值得本地跑通的三个理由把一份带设计文档的 Java Swing 进销存管理系统源码跑起来是我认为 Java 学习者从语法过渡到项目能力最短的一段路。它没有 Spring 全家桶帮你把对象装配好所有窗口、监听器、数据库连接都是裸代码你能清清楚楚看到一次按钮点击背后发生的全部事情。同时进销存这个业务本身闭环完整商品、客户、供应商、采购入库、销售出库、库存盘点、报表统计每一步都有数据流转比单纯练增删改查高一个层次。这三件事叠加在一起使它同时适合三类人准备毕业设计的在校生、刚学完 JavaSE 想找项目练手的开发者以及要给小商户交付一套离线单机管理工具的自由职业者。这套源码的核心价值不在界面美观而在把业务流程翻译成代码的完整路径。2. 读源码先看结构Swing进销存项目的标准三层划分和关键文件清单2.1 靠包名就能定位职责的源码目录树拿到压缩包解压后先别急着点运行按钮。用 IDE 打开把src目录整体看一遍。带设计文档word 或 pdf的 Swing 项目代码组织通常不是平铺的而是按包名区分职责。我见过的大多数进销存项目目录长这样src/main/java/com/example/erp ├── model/ # 实体类Goods.java, Customer.java, Supplier.java, StockBill.java ├── dao/ # 数据访问层GoodsDao.java, StockDao.java, UserDao.java ├── service/ # 业务层PurchaseService.java, SaleService.java, StockService.java ├── ui/ # 界面层MainFrame.java, GoodsPanel.java, SalePanel.java ├── util/ # 工具类DBUtil.java, DateUtil.java, ExcelUtil.java └── resources/ └── db/stock.sql # 建表脚本和初始化数据 docs/ # 设计文档需求说明书.docx, 数据库设计文档.docx, 概要设计.docx这段目录树的作用不是让你背路径而是建立第一个判断这个项目是否遵循了最基本的关注点分离。model里的实体类只存数据dao只写 SQLservice负责事务和业务规则ui只做界面展示和事件监听。如果某个项目把 SQL 写进了ui包里的按钮点击方法后续所有功能都会变成一团乱麻。你拿到源码后第一件事就是在dao包里找数据库操作在service包里找事务边界这是最快的熟路方式。2.2 设计文档里最先应该读的三处ER图、表清单和流程说明压缩包里的设计文档不是摆设。很多人在读源码前忽略它结果花两小时猜一个字段含义。我一般会按下面这个顺序读文档配合源码里的类名去对照效率最高文档章节重点看什么对应源码位置数据库设计ER 图、每张表的字段注释resources/db/stock.sql业务流程图采购入库/销售出库的步骤描述service包中的事务方法功能清单系统有哪些模块、角色权限ui包中的MainFrame.java以数据库设计文档为例它通常会写明stock_bill表里bill_type字段的取值含义0 代表采购入库1 代表销售出库2 代表盘点调整。如果你不读文档看到一堆 0 和 1 的数字会以为代码写错了。另外文档里的 ER 图会告诉你表和表之间的关系商品表和入库单据明细是 1 对 N客户表和销售单据是 1 对 N库存表是商品表的扩展。理解了这些关系你就知道为什么查询销售记录时要 JOIN 三张表而不是一张。2.3 实体类和表字段不一致时以哪边为准这是 Swing 项目里最常见的历史包袱实体类里的属性叫goodsName数据库字段叫g_name实体类用String createTime接日期数据库里是datetime类型。遇到这种情况以数据库表结构为准因为 DAO 层的 SQL 是靠数据库字段名查数据的。// GoodsDao.java 中的一段典型查询 public ListGoods findAll() { String sql SELECT id, g_name, spec, unit, price, stock FROM goods; // 执行查询把 ResultSet 逐行封装为 Goods 对象 }这段代码里g_name是数据库字段goods.setName(rs.getString(g_name))把字段值赋给实体属性。你改造实体类时不要乱改数据库字段名否则整条链路上的 SQL 都要跟着改。正确做法是保留数据库命名不动在实体类属性上写注释标注对应关系或者用alias让 SQL 返回的列名和实体属性一致。这是读这类源码时最实用的一个习惯。3. 采购入库、销售出库与超卖控制进销存核心逻辑的代码对照3.1 采购入库先记流水还是直接改库存进销存的核心不是界面而是库存数据一致性。以采购入库为例常见的实现顺序有两种先更新库存表的stock字段再插入流水明细或者先插明细再改库存。这两种顺序在单机单事务下结果一样但前一种更接近手工记账的习惯——货先到库存先加上账目随后补。public boolean addPurchase(PurchaseBill bill, ListPurchaseItem items) { Connection conn null; PreparedStatement psBill null; PreparedStatement psItem null; PreparedStatement psStock null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务保证所有操作一起成功或一起失败 // 1. 插入入库单主表 psBill conn.prepareStatement( INSERT INTO stock_bill(bill_no, bill_type, supplier_id, amount, create_time) VALUES(?,?,?,?,?)); psBill.setString(1, bill.getBillNo()); psBill.setInt(2, 1); // 1 代表采购入库对应设计文档的枚举约定 psBill.setInt(3, bill.getSupplierId()); psBill.setBigDecimal(4, bill.getAmount()); psBill.setTimestamp(5, new Timestamp(System.currentTimeMillis())); psBill.executeUpdate(); // 2. 逐条更新库存并把明细写入子表 for (PurchaseItem item : items) { psStock conn.prepareStatement( UPDATE goods SET stock stock ? WHERE id ?); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getGoodsId()); psStock.executeUpdate(); psItem conn.prepareStatement( INSERT INTO stock_bill_item(bill_id, goods_id, quantity, price) VALUES(?,?,?,?)); psItem.setInt(1, bill.getId()); psItem.setInt(2, item.getGoodsId()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getPrice()); psItem.executeUpdate(); } conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { // 用 try-with-resources 统一释放资源避免连接泄漏 } }这段代码的关键是事务边界的设定setAutoCommit(false)之后三次数据库操作要么全部提交要么全部回滚。如果漏掉这行前半截已经更新库存、后半截插入明细失败就会出现账实不符。参数说明里要注意bill_type这个参数直接控制单据类型不同取值在报表里会被统计到不同栏目quantity参数在入库时是正数加进库存后续做退货时传负数即可冲减。3.2 销售出库用条件 UPDATE 防超卖比起采购入库销售出库更怕的是超卖——客户下单买 100 件库存只剩 80 件你还让交易成功了。新手常见的做法是先查库存判断够了再扣减但这个查和改之间隔着时间窗口多线程并发下会出事。单机 Swing 项目虽然并发量不高但你用 MySQL 做后端、多台收银机同时连库时问题就会暴露。// 用一条带条件的 UPDATE 原子地完成“检查并扣减” String sql UPDATE goods SET stock stock - ? WHERE id ? AND stock ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, saleQuantity); // 本次销售数量 ps.setInt(2, goodsId); // 商品主键 ps.setInt(3, saleQuantity); // 条件当前库存不小于销售数量 int rows ps.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足销售失败); }这段代码的精髓在WHERE stock ?数据库在更新时的行级锁会把检查和扣减合并成一个原子操作不会出现两个人同时读到库存 100、各自扣 80 的极端情况。如果executeUpdate()返回 0说明条件不满足直接抛异常让事务回滚。顺带一提这个写法也解决了把负数库存坐实的问题不需要额外写 if 判断逻辑更干净。saleQuantity在这里既是扣减数量也是条件阈值注意不要把它和goodsId的位置写反否则库存数据会被改乱。3.3 库存台账、盘点差异和负数单据的边界进销存项目里还容易混淆三个概念库存表是当前值流水表是变化轨迹盘点单是修正依据。库存表goods.stock只存当前的最终数量它是被计算出来的结果stock_bill_item每插入一条记录就意味着库存发生过一次变化它是审计依据。盘点时不直接改goods.stock了事而是要生成一张盘点单记录账面数和实盘数的差额再把这个差额做成一张调整单据。常见做法是先查账面库存和手工数对比差异数生成一条adjust类型流水再更新库存表的当前值。报表里的“库存变动表”查的就是流水不是库存表。如果你只更新库存表不写流水月底对账时差异就是一笔糊涂账。4. 从JTextField到JTableSwing界面事件与业务层的数据联动4.1 TabbedPane挂载业务面板的MainFrame框架Swing 进销存项目的主窗口多半是一个JFrame里面放一个JTabbedPane每个 Tab 对应一个业务模块。这种布局的好处是模块间互不干扰加新功能时只需要再挂一个面板不用改动其他代码。public MainFrame() { setTitle(进销存管理系统); setSize(1200, 800); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JTabbedPane tabbedPane new JTabbedPane(); tabbedPane.addTab(商品管理, new GoodsPanel()); tabbedPane.addTab(采购入库, new PurchasePanel()); tabbedPane.addTab(销售出库, new SalePanel()); tabbedPane.addTab(库存查询, new StockPanel()); tabbedPane.addTab(报表统计, new ReportPanel()); add(tabbedPane, BorderLayout.CENTER); }这段代码的注意点有两个addTab的参数顺序是标题和组件前者是显示在标签上的文字后者是具体的面板实例所有业务面板都继承JPanel所以它们可以被塞进同一个 Tab。面板之间不要互相new实例来访问数据应该有业务层做中转比如SalePanel在销售成功后调用StockPanel.refreshData()刷新库存表格这个刷新方法应当是公共的。4.2 DefaultTableModel 是表格数据的关键别直接操作 JTable 数据JTable 本身不存数据它只负责渲染。真实数据放在TableModel里而 Swing 进销存项目里最常用的是DefaultTableModel。很多人把数据直接塞进 JTable 后想动态新增一行结果发现界面不刷新就是因为没走 TableModel 的接口。private void loadGoodsData() { ListGoods list goodsService.findAll(); String[] columns {编号, 商品名称, 规格, 单位, 进价, 售价, 库存}; Object[][] rows new Object[list.size()][columns.length]; for (int i 0; i list.size(); i) { Goods g list.get(i); rows[i][0] g.getId(); rows[i][1] g.getName(); rows[i][2] g.getSpec(); rows[i][3] g.getUnit(); rows[i][4] g.getPurchasePrice(); rows[i][5] g.getSalePrice(); rows[i][6] g.getStock(); } DefaultTableModel model new DefaultTableModel(rows, columns); model.setRowCount(0); // 清空旧数据防止刷新时残留 for (Object[] row : rows) { model.addRow(row); // 逐行填充新数据 } goodsTable.setModel(model); // 重新绑定数据模型 }这段代码里最容易被忽略的是model.setRowCount(0)。如果不先清空多次点击查询按钮后表格会出现重复内容——因为重新 setModel 并不会自动清掉旧的行数据。columns数组决定了表头文字和实体类的属性没有必然对应关系你可以只展示 7 个字段而实体类里有 10 个属性。这也是 Swing 比某些 Web 框架灵活的地方展示层和数据层解耦。刷新表格时不要再 new 一个监听器粒度太粗正确思路是抽出refreshData()方法供其他模块调用。4.3 搜索、日期范围和下拉联动进销存界面的查询条件一般有三种关键字模糊搜索、日期区间筛选、分类下拉联动。这三件事的核心都是在DocumentListener或ActionListener里触发一次查询然后把结果重新灌进表格。tfKeyword.getDocument().addDocumentListener(new DocumentListener() { public void insertUpdate(DocumentEvent e) { search(); } public void removeUpdate(DocumentEvent e) { search(); } public void changedUpdate(DocumentEvent e) { search(); } });search()内部调用goodsService.search(keyword, startDate, endDate)这些参数最终在 DAO 层变成WHERE g_name LIKE ? AND create_time BETWEEN ? AND ?用PreparedStatement的setString和setTimestamp填值。需要留意的参数细节是LIKE语句里的百分号在 Service 层拼接好% keyword %再传进去防止在 SQL 里写死导致注入风险。日期参数要选对类型界面拿到的是java.util.Date传给 JDBC 前转换成java.sql.Timestamp否则会丢时分秒。5. 进销存数据库设计与JDBC连接细节先看设计文档能少踩的坑5.1 进销存最少要六张表从ER草稿到正式表结构设计文档里的 ER 图看着复杂落到建表脚本时其实有一条主线商品表库存、往来单位表客户/供应商、单据表出入库流水、单据明细表子表。一套能跑的进销存项目最简表结构至少是这样CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, g_name VARCHAR(50) NOT NULL COMMENT 商品名称, spec VARCHAR(30) COMMENT 规格型号, unit VARCHAR(10) COMMENT 计量单位, purchase_price DECIMAL(10,2) COMMENT 进价, sale_price DECIMAL(10,2) COMMENT 售价, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存 ); CREATE TABLE stock_bill ( id INT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(30) UNIQUE COMMENT 单据编号, bill_type TINYINT COMMENT 1采购 2销售 3调整, supplier_id INT COMMENT 供应商或客户ID, amount DECIMAL(10,2) COMMENT 单据总金额, create_time DATETIME COMMENT 单据时间 ); CREATE TABLE stock_bill_item ( id INT PRIMARY KEY AUTO_INCREMENT, bill_id INT COMMENT 主表外键, goods_id INT COMMENT 商品外键, quantity INT COMMENT 数量, price DECIMAL(10,2) COMMENT 当时单价 );这张表设计的核心点是金额字段用DECIMAL(10,2)而不是float或double。二进制浮点数在累加过程中会产生精度误差账目算不平是所有财务类系统的红线。数量字段用INT够用但如果你的商品可能是按重量卖要改成DECIMAL(10,3)。stock_bill和stock_bill_item是一对多关系前者记录“发生了什么事”后者记录“具体哪些货”。设计文档里如果写了“库存表冗余了库存数量字段”那是因为查询性能考虑这个冗余字段由业务层在每次出入库时同步更新。5.2 JDBC URL参数时区、编码和连接超时每次看到新手在本地 MySQL 跑通、换一台电脑就连不上的情况十有八九是 JDBC 连接串没写全。Swing 项目通常用DriverManager直连不走连接池这时 URL 的每个参数都得写对private static final String URL jdbc:mysql://localhost:3306/erp_db?useUnicodetrue characterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 123456; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); }逐个参数说清楚useUnicodetrue和characterEncodingutf8是配套的保证中文写入和读取都不乱码serverTimezoneAsia/Shanghai是 Java 8 之后必填项MySQL 8.x 驱动默认要求时区明确否则抛The server time zone value 是中国标准时间的异常useSSLfalse是为了避免本地调试时 SSL 握手告警刷屏。localhost:3306是数据库地址和端口如果你的库装在虚拟机里要改成虚拟机的 IP。这个getConnection()方法在 DAO 层会被频繁调用不要把 Connection 做成单例静态字段用完即关否则客户端操作一段时间后就报告Too many connections。5.3 用PreparedStatement而不是字符串拼接老项目里残留的字符串拼接 SQL 是改造成本最高的地方。如果你在源码里看到下面这类写法必须动手改掉// 危险写法直接拼用户输入 String sql SELECT * FROM goods WHERE g_name LIKE % keyword %; Statement st conn.createStatement(); ResultSet rs st.executeQuery(sql);这种写法有两个致命问题一是单引号在拼接时会破坏 SQL 语法输入a OR 11就能把整个商品表带出来这是一个广为流传的注入套路二是字符串拼接的 SQL 每次都会让数据库重新解析效率低于预编译的 PreparedStatement。安全的版本是String sql SELECT * FROM goods WHERE g_name LIKE ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); ResultSet rs ps.executeQuery();这个写法里?是占位符setString(1, ...)把第一个问号替换成带单引号转义的参数——无论用户输入什么都只被当作字面量不再参与 SQL 语法解析。把条件换成WHERE sale_price BETWEEN ? AND ?时第一个问号是区间下限第二个是上限顺序不能反。改造这类代码时顺带检查一下有多少地方用了SELECT *按需列出字段能在表结构大时减少网络传输量。6. 本地跑通后改图标、打安装包和中文乱码的一次性处理源码能跑只是第一步真正交付给用户使用还要处理三个看起来很碎、但不解决就上不了台面的问题字体和文件名乱码、窗口图标太丑、双击.bat启动太低级。中文乱码分两种。第一种是运行界面乱码原因是 Java 文件保存编码和编译编码不一致。如果你用javac手动编译加参数-encoding UTF-8如果 IDE 是 Eclipse把 Window 菜单里的Text file encoding改为 UTF-8IntelliJ 则在设置里搜索file encoding统一三处编码。第二种是打包成 jar 运行后乱码这通常是因为 manifest 或启动参数没带编码选项在启动命令里加上-Dfile.encodingUTF-8即可。窗口图标替换成自己设计的 logo只需在MainFrame构造方法里加一行setIconImage(new ImageIcon(resources/logo.png).getImage());注意resources是相对路径jar 包运行时当前目录可能是任意地方稳妥做法是把图片放进 classpath用getClass().getResource(/logo.png)加载。getImage()返回的是 Image 对象setIconImage只改窗口左上角图标任务栏图标同时生效。给客户打包安装程序Java 8 时代常用 exe4j 或 InstallShield如果你的 JDK 版本在 14 以上直接用官方jpackage命令最省事# 先打成普通可执行 jar再生成 Windows 安装包 mvn clean package jpackage --name erp-system \ --input target/ \ --main-jar erp-system.jar \ --main-class com.example.erp.ui.MainFrame \ --type msi \ --dest dist/jpackage的参数含义--input指定依赖库目录--main-jar是主程序 jar 包--main-class是带main方法的入口类--type msi生成安装向导格式如果要免安装版把msi改成app-image。这个命令打出来的包自带运行环境目标机器不需要预装 JDK这对给客户部署是决定性的差异。打完包后用jlink裁掉没用的模块安装包体积能从 100MB 缩到 40MB 左右。注意打包前先确认MainFrame里的数据库连接地址指向的不是你自己本地的localhost而是客户机器的数据库 IP否则安装完还是连不上。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价