ARTICLE DETAIL

资讯详情

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

Java Swing 进销存系统实战:数据库设计、事务与界面避坑指南

Java Swing 进销存系统实战:数据库设计、事务与界面避坑指南 简介面向Java Swing与SQL Server 2000数据库学习者的进销存管理系统源码包适用于课程设计、毕业设计或中小型企业的进销存业务改造与二次开发。系统完整覆盖信息管理、业务管理、库存管理、查询统计与系统管理五个核心模块客户/商品/供应商档案维护进货单、销售单的业务流转库存盘点操作以及客户、商品、销售、入库等维度的查询统计并提供操作员权限管理。压缩包共325个文件包含75个java源文件、169个class编译产物、56个png界面资源另有doc/ppt设计文档、jar运行依赖库和mdf/ldf数据库文件其中数据库文件为SQL Server 2000备份整包约4.47MB。已有600人学习下载适合通过完整项目实践理解Swing界面与SQL Server 2000数据库的整合开发方式也可作为进销存课程设计的参考模板。资源导入MyEclipse环境后可结合源码和设计文档梳理从数据表建模、DAO数据访问到界面交互的完整流程具备较好的参考价值。1. Java Swing 进销存管理系统为什么老技术还有人用它做库存管理看到「Java Swing 进销存管理系统源码」这个标题很多人第一反应是「Swing 不是早就过时了吗」。但真实情况是只要去企业级服务市场看一眼大量中小型贸易公司、连锁门店、仓库管理场景里跑得最稳的反而就是这类基于 Swing 的桌面端进销存系统。它们不开浏览器、不依赖 Tomcat双击启动、数据落在本地或局域网数据库里一套系统能用八到十年不换。Java Swing 进销存管理系统这个方向能持续有人找、有人学、有人维护核心原因有三点交付形态简单打包成 jar 或 exe 就能发、业务模型成熟进销存三个字背后是一套固定的表结构和操作流、以及改造成本低——拿到源码后不用理解分布式和微服务改个界面、加个字段就能上线。这套方案最适合两类人一是准备做 Java 课程设计或毕业设计的在校生二是要给小商户做信息化的外包开发者。2. 开局先建表进销存数据模型怎么设计才不返工进销存系统的代码可以写得风格各异但数据库表结构基本是同一套骨架。如果拿到源码后第一件事就是跑起来看界面那后面改起来会非常难受。正确顺序是先读设计文档里的 ER 图和数据字典再对着表结构理解业务流。很多所谓的「进销存源码」在业务上偷工减料只做了个商品增删改查加一个库存数字字段那本质上是 Excel 搬家不是进销存。2.1 库存表为什么不能只存一个数量字段最常见的翻车设计是商品表里直接放一个stock_quantity字段入库就UPDATE加一下出库就减一下。这个方案在单机、单用户、数据量几千条的演示环境里跑得通但一上真实业务就暴露三个问题无法追溯库存变化历史、无法做先进先出核算、无法回答「这个月的损耗去哪了」这种盘点问题。正规做法是把「库存余额」和「库存流水」分开。商品表里的库存只是冗余汇总字段每次业务操作必须同时写一张库存流水表记录批次号、变动类型、变动数量、变动前后余额、关联单据号和操作时间。这也是为什么很多 Java 面试题里会问「如何保证数据一致性」——在进销存场景里答案就是事务加流水表对账。CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(32) NOT NULL UNIQUE COMMENT 商品编码, name VARCHAR(128) NOT NULL COMMENT 商品名称, spec VARCHAR(64) COMMENT 规格型号, unit VARCHAR(16) NOT NULL DEFAULT 件, purchase_price DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 采购价, sale_price DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 销售价, stock_quantity INT NOT NULL DEFAULT 0 COMMENT 冗余库存余额, 可通过对账修正, safety_stock INT NOT NULL DEFAULT 0 COMMENT 安全库存预警线, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock_ledger ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, batch_no VARCHAR(32) NOT NULL COMMENT 批次号, 格式: yyyyMMddHHmmss 三位随机数, change_type TINYINT NOT NULL COMMENT 1入库 2出库 3盘点调整 4退货入库 5报损出库, change_qty INT NOT NULL COMMENT 正数增加 负数减少, before_qty INT NOT NULL COMMENT 变动前结存, after_qty INT NOT NULL COMMENT 变动后结存, ref_bill_type TINYINT COMMENT 关联单据类型 1采购单 2销售单 3盘点单, ref_bill_no VARCHAR(32) COMMENT 关联单据编号, remark VARCHAR(255), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_product_time (product_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关于字段类型说明价格用DECIMAL(12,2)而不是FLOAT这个在进销存里是必修课——浮点数计算会累积误差卖一百件单价 9.9 的商品用FLOAT算总额可能是 989.99999。进销存任何涉及金额和数量的字段要么DECIMAL要么BIGINT以分或克为最小单位存储这是 Java 基础里的数据类型选择问题也是面试里常被追问的点。2.2 单据主从表结构进货单和销售单怎么挂明细进销存和普通 CRUD 的最大区别在于「单据语义」。一次入库不是一个简单 INSERT而是一张话单头部 若干商品明细行项目 库存变动流水三者必须同生共死。源码里如果只有stock_in一张表把所有商品塞在一个字段里那是演示项目没法上生产。主从表设计应该拆成purchase_order采购入库单头、purchase_order_item入库明细、sale_order销售单头、sale_order_item销售明细四张核心业务表。单头存储往来单位、单据编号、业务员、过账状态明细存储商品、数量、单价、金额、批次号。CREATE TABLE purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_no VARCHAR(32) NOT NULL UNIQUE COMMENT 单据号, 建议规则: PO yyyyMMdd 流水号, supplier_id INT NOT NULL COMMENT 供应商ID, 对应supplier表, operator VARCHAR(32) NOT NULL COMMENT 入库操作人, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已过账 2已作废, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 单头冗余汇总金额, remark VARCHAR(255), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME COMMENT 过账时间, 过账操作时填充 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE purchase_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_id BIGINT NOT NULL COMMENT 关联purchase_order.id, product_id INT NOT NULL, batch_no VARCHAR(32) NOT NULL COMMENT 本行商品产生的批次号, qty INT NOT NULL COMMENT 入库数量, price DECIMAL(12,2) NOT NULL COMMENT 实际入库单价, amount DECIMAL(12,2) NOT NULL COMMENT 金额 qty * price, 由程序计算后写入 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意purchase_order_item里没有再加product_name或者unit之类的冗余字段——商品名称、规格、单位全部从product表关联查询。很多新手在设计表时会想把名称冗余进明细表方便查询但如果商品改名或停用历史单据跟着变后面财务对账就是一场灾难。正确的做法是单据快照语义价格、数量必须留在明细表名称、规格这种基础资料属性不冗余随时 JOIN 取最新值。在设计文档里还应当有「过账」这个概念。这里要特别说明业务上入库不是数据录完就生效而是经过审核。未审核的单据允许修改甚至删除审核过账后库存正式变动并封存。很多入门的进销存源码没有状态位数据一存就加库存这在业务上是行不通的——录错了只能改数据库时间一长系统就变成黑匣子。3. 用 Swing 搭出货架JTable 为核心的主界面与多窗口跳转进销存系统的界面核心是一个主窗口JFrame 菜单栏 若干功能面板JPanel所有业务功能通过卡片式布局切换或弹窗完成。源码里最常见的架构是MainFrame持有全局CardLayout左侧树形菜单点击时切换右侧内容面板。Swing 组件里工作量最大、最容易写差的是JTable因为商品列表、单据明细、库存查询全部要依赖它展示和编辑。3.1 商品管理面板JTable 数据模型的三段式刷新很多刚接触 Swing 的开发者写表格会直接在JFrame里 new 一个JTable然后table.setModel(new DefaultTableModel(data, headers))。项目小的时候没问题但进销存的表格有一个特点数据量大、列类型复杂金额列要右对齐、数量列要可编辑但库存列只读用DefaultTableModel会把所有单元格当字符串处理排序和渲染都很别扭。推荐做法是自定义TableModel继承AbstractTableModel把数据源从二维数组换成ListProduct对象列表。这样改一行商品数据时只需要操作 List 里的对象然后调用fireTableDataChanged()通知表格刷新。public class ProductTableModel extends AbstractTableModel { private ListProduct products; private String[] columns {编码, 名称, 规格, 单位, 采购价, 销售价, 库存, 状态}; public ProductTableModel(ListProduct products) { this.products products; } Override public int getRowCount() { return products.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) { Product p products.get(rowIndex); switch (columnIndex) { case 0: return p.getSkuCode(); case 1: return p.getName(); case 4: return p.getPurchasePrice(); // BigDecimal 类型, 通过渲染器右对齐 case 6: return p.getStockQuantity(); default: return ; } } Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; // 商品列表一律只读, 编辑走单独的商品编辑对话框 } Override public Class? getColumnClass(int columnIndex) { // 返回类型后, JTable 才能正确调用对应渲染器 if (columnIndex 4 || columnIndex 5) return BigDecimal.class; if (columnIndex 6 || columnIndex 7) return Integer.class; return String.class; } }这段代码里最关键的是重写getColumnClass方法并返回BigDecimal.class和Integer.class否则 JTable 默认把单元格内容当字符串渲染看起来数字靠左点击排序时还会按字典序给 10 排在 9 前面。正确返回类型后金额右对齐、数字可比较由默认渲染器直接接管。查询商品时只要ListProduct list service.queryAll(); tableModel.setData(list); tableModel.fireTableDataChanged();三行完成刷新不用像操作DefaultTableModel一样逐行删除重建。3.2 多窗口协作主窗口持有 Service 层单例进销存系统里主窗口、商品编辑框、进货单选商品弹窗之间要共享数据库连接和业务服务。源码里比较典型的架构是 Service 层用单例模式暴露给所有窗口窗口之间不直接依赖而是通过构造函数传入需要的 Service 对象。public class MainFrame extends JFrame { private CardLayout cardLayout; private JPanel cardPanel; private ProductService productService; private StockService stockService; public MainFrame() { productService new ProductService(); // 内部持有 Dao 实例 stockService new StockService(); initUI(); } private void initUI() { setTitle(进销存管理系统 — 主工作台); setSize(1024, 768); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); cardLayout new CardLayout(); cardPanel new JPanel(cardLayout); // 左侧菜单栏 JTree menuTree buildMenuTree(); menuTree.addTreeSelectionListener(e - { String nodeName e.getPath().getLastPathComponent().toString(); cardLayout.show(cardPanel, nodeName); // 根据菜单名切卡片 }); // 右侧内容卡片容器 cardPanel.add(new ProductPanel(productService), 商品管理); cardPanel.add(new StockInPanel(productService, stockService), 采购入库); cardPanel.add(new StockOutPanel(productService, stockService), 销售出库); cardPanel.add(new StockQueryPanel(productService, stockService), 库存查询); getContentPane().setLayout(new BorderLayout()); getContentPane().add(new JScrollPane(menuTree), BorderLayout.WEST); getContentPane().add(cardPanel, BorderLayout.CENTER); } }这个架构的好处是每个业务面板自治面板之间不互相 new而是共享主窗口里持有的 Service 实例。比如采购入库面板录入完一张入库单并过账后如果这时切到库存查询面板查询面板持有的stockService查询的是同一份数据源天然一致。Swing 组件本身不是线程安全的这种单例 Service 配合后续第 5 章要讲的「事件分发线程」规则是保证界面不卡顿、数据不串台的基本前提。JTree做菜单时有个隐藏细节叶子节点点击要在TreeSelectionListener里判断isLeaf()只对叶子触发卡片切换否则点击「基础资料」这种父节点时右侧会切到一个不存在的卡片。这个坑在拿到源码改造时很容易踩cardLayout.show()的第二个参数对应的名字必须和add(component, name)时一致写错一个汉字就白屏。4. 入库、出库、盘点业务操作怎么落到代码里且事务不穿洞进销存的业务操作核心代码不在界面而在 Service 层的一个个doXxx方法里。以采购入库为例完整动作链是写入单头 → 写入明细 → 逐行更新库存余额 → 逐行写库存流水 → 更新单头状态为已过账。任何一步失败前面所有写入都要回滚否则会出现「明细有了但库存没加上」的脏数据。4.1 入库过账的事务边界一个方法只干一件事很多源码翻车就翻在这里。有些项目把上述逻辑拆到几个Dao方法里在界面层先调insertBill()再循环调updateStock()最后调updateStatus()。用户看着像完成了但中间任何一步报错前面已提交的数据就留在库里而且没有任何提示。等到盘点时发现账实不符只能靠手工改数据库这种痛苦做过进销存维护的人都有切身体会。正确做法是把整个过账动作放进一个Transactional语义的方法里。Swing 项目里不引入 Spring 也可以手动控制事务边界核心思路是拿到Connection后设置setAutoCommit(false)所有 Dao 方法都传入同一个Connection最后统一 commit 或 rollback。public class StockService { private DataSource dataSource; public boolean stockIn(Connection conn, PurchaseOrder order, ListPurchaseOrderItem items) throws SQLException { // conn 由外部传入, 且调用方保证 setAutoCommit(false) try { // 1. 写入单头 insertPurchaseOrder(conn, order); // 2. 写入明细, 每个明细行生成批次号并更新库存 for (PurchaseOrderItem item : items) { item.setBatchNo(generateBatchNo(item.getProductId(), item.getQty())); insertPurchaseOrderItem(conn, item); // 3. 库存增加: UPDATE product SET stock_quantity stock_quantity ? WHERE id ? updateStockIncrease(conn, item.getProductId(), item.getQty()); // 4. 写库存流水, 记录变动前后余额 insertStockLedger(conn, item.getProductId(), item.getBatchNo(), 1, item.getQty(), item.getQty() /* before_qty 需要在第一步先查出来 */); } // 5. 更新单头状态为已过账 updateOrderStatus(conn, order.getId(), 1); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); log.error(采购入库过账失败, 业务动作全部回滚, billNo{}, order.getBillNo(), e); throw e; } } }上面的代码展示了一个容易被忽略的细节updateStockIncrease之前必须先把当前库存查出来否则stock_ledger里的before_qty和after_qty只能填 0后面做数据对账就废了。推荐用一条 SQL 原子更新同时返回更新前后的值UPDATE product SET stock_quantity stock_quantity ? WHERE id ?然后紧接着SELECT stock_quantity FROM product WHERE id ?查回更新后的值在同一个事务内这两个操作之间不会被其他会话插队前后余额才是准确的。4.2 单据编号生成并发下单为什么会出现重复单号单据编号规则看着简单实际是个高频翻车点。用System.currentTimeMillis()拼单号在单机演示时永远不重复但真实仓储环境下两个人同时点「过账」时间戳毫秒级相同会导致主键唯一约束报错整单回滚。稳妥做法是维护一张独立的编号流水表每次取号时对行加锁。Swing 桌面系统并发量有限用数据库SELECT ... FOR UPDATE取号完全不丢性能且是万无一失的方案。单号规则建议PO yyyyMMdd 三位当天自增号每天的编号从 001 重新累加这样月末导数据时按单号排序即可还原当天流水。-- 编号流水表 CREATE TABLE sequence_bill ( bill_type VARCHAR(4) NOT NULL COMMENT PO:采购入库 SO:销售出库 PD:盘点, biz_date DATE NOT NULL COMMENT 业务日期, seq INT NOT NULL DEFAULT 0, PRIMARY KEY (bill_type, biz_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;public String generateBillNo(Connection conn, String billType) throws SQLException { String today LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String key billType today; // 行级锁: 同一类型同一天只有一个会话能进入更新 PreparedStatement ps conn.prepareStatement( INSERT INTO sequence_bill (bill_type, biz_date, seq) VALUES (?, CURRENT_DATE, 1) ON DUPLICATE KEY UPDATE seq seq 1 ); ps.setString(1, billType); ps.executeUpdate(); PreparedStatement ps2 conn.prepareStatement( SELECT seq FROM sequence_bill WHERE bill_type ? AND biz_date CURRENT_DATE ); ps2.setString(1, billType); ResultSet rs ps2.executeQuery(); rs.next(); int seq rs.getInt(1); return billType today String.format(%03d, seq); }这段代码里用了ON DUPLICATE KEY UPDATE配合主键(bill_type, biz_date)做原子递增比先SELECT再UPDATE少了一次并发窗口。取号操作必须在过账事务的最前面执行一旦进入业务后续操作发现失败回滚单号已经被消费掉正好保证数据不存在「单据号相同但内容不同」的脏数据。单号空缺是合规现象不用补。4.3 盘点损溢为什么直接改库存等于自毁前程盘点流程的核心业务规则是「先冻结再生成损溢单」。源码里常见的错误写法是盘点时直接UPDATE product SET stock_quantity realQty完全不记录账面数、实数、差异数。这会导致月底财务喊你解释库存差异时你翻遍日志看不到任何痕迹。规范设计是建盘点单头存盘点时间、经手人、备注盘点行明细存商品、账面库存盘点时刻快照、实盘库存、差异数量。审核盘点单时再执行「损溢调整」实盘大于账面走盘盈入库实盘小于账面走盘亏出库两者都生成库存流水变动类型标记为 3。这么做的好处是利润报表里能体现损耗成本而不是稀里糊涂少了一批货。「账面库存快照」这个细节尤其重要。盘点是按商品分类进行的通常耗时几个小时这期间可能有正常入库出库单发生。如果不在开单时锁定账面数等审核时再读库存得出的差异会被期间的业务干扰。如果系统并发不高直接在这里加SELECT stock_quantity FROM product WHERE id ? FOR UPDATE锁住该商品行直到盘点单过账完成才释放是最直接有效的方案。5. 避坑笔记Swing 线程、JTable 刷新与 SQL 事务的 5 个翻车现场以下追的这些坑是我拿一套典型的 Swing 进销存源码改造上线时逐个踩过的每一条都对应一个实际报错或者用户投诉场景不是从概念推导出来凑数的。5.1 现象点「导出报表」按钮后界面白屏转圈鼠标变沙漏原因所有数据库查询都在 Swing 事件分发线程EDT里同步执行。导出几千条数据时SELECT加上写 Excel 耗时两三秒EDT 被阻塞期间按钮不响应、窗口无法拖动关闭用户体验就是「卡死」。解决耗时操作丢到SwingWorker后台线程执行。doInBackground()里做数据库查询和 Excel 写入done()里回调 UI 刷新。注意done()是回到 EDT 执行的不需要再手动SwingUtilities.invokeLater。SwingWorkerVoid, Void worker new SwingWorkerVoid, Void() { Override protected Void doInBackground() { excelFile exportService.exportStockReport(startDate, endDate); return null; } Override protected void done() { JOptionPane.showMessageDialog(MainFrame.this, 导出成功: excelFile.getAbsolutePath(), 报表导出, JOptionPane.INFORMATION_MESSAGE); } }; worker.execute();5.2 现象新增商品保存后列表里看不到新数据必须重启程序原因界面拿到的ProductTableModel持有的是旧ListProduct引用Service 层查询后重新new ArrayList(...)表格模型里的引用和数据源已经脱节。新数据进了数据库但模型里的 List 还是旧数据。解决在表格模型里加一个setData(ListProduct newData)方法内部把 List 引用替换并调用fireTableDataChanged()任何页面在做完增删改后都必须执行「重新查询 setData 刷新」三连。这个习惯一旦养成后面做库存界面、单据明细界面都不会再遇到数据不刷新。5.3 现象库存数量和进销存报表总金额偶尔出现「差一分钱」原因数据库字段用了DECIMAL(12,2)没问题是但代码里用double做金额运算后再 set 进BigDecimal字段。Java 基础问题0.1 0.2在二进制浮点数里是0.30000000000000004存储时四舍五入丢了一位报表汇总时误差累积。解决所有金额计算一律BigDecimal禁止任何double参与业务运算。UI 层输入框拿到字符串直接new BigDecimal(inputText)两个金额相加用.add()乘法用.multiply()。如果从数据库查到BigDecimal要做除法保留两位小数一定传MathContext.DECIMAL64或指定scale和RoundingMode。5.4 现象出库单过账提示「库存不足」但查看库存明明够原因先查到可用库存放在界面上用户停留了五分钟慢慢录入出库信息期间另一台电脑销售了同款商品等到点过账时库存已被扣减。界面看到的是五分钟前的过期数据数据库当前余额已不足。解决出库过账时把库存扣减语句写成原子条件更新UPDATE product SET stock_quantity stock_quantity - ? WHERE id ? AND stock_quantity ?。如果更新行数为 0说明库存不足或商品被锁立即回滚并提示用户重新确认。这比先查后改多了一次数据库判断代价极小但彻底消灭了超卖。-- 出库扣减: 条件更新, 返回 0 行即库存不足 UPDATE product SET stock_quantity stock_quantity - ? WHERE id ? AND stock_quantity ?;5.5 现象JTree 点击菜单偶尔不响应点第二次才切换原因TreeSelectionListener在收起父节点时也会触发此时getLastPathComponent()可能是父节点直接cardLayout.show()找不存在的 key 静默失败界面没有任何反馈。解决在监听器里判断选中的节点是否为叶子只有叶子节点才做卡片切换。另外CardLayout.show()的 key 必须与add()时的组件名完全一致——多一个空格或少一个汉字都不会报错但切换无效这种「代码没报错但功能不生效」的问题最浪费调试时间建议所有卡片 key 定义成常量类统一管理。6. 上线前最后一步用 1000 行数据压测库存一致性并顺手优化启动速度拿到源码后不建议立刻替换正式业务数据先在临时库导入 1000 条商品、5 万条流水做一轮完整性压测。压测核心不是看界面流不流畅而是验证「库存余额 流水累计」这条恒等式是否成立。数据库里写一条校验 SQL跑出差异就说明事务有洞或者有人绕过系统手工改过表。SELECT p.id, p.sku_code, p.stock_quantity AS balance_qty, COALESCE(l.ledger_qty, 0) AS flow_qty, (p.stock_quantity - COALESCE(l.ledger_qty, 0)) AS diff_qty FROM product p LEFT JOIN ( SELECT product_id, SUM(CASE WHEN change_type IN (1, 4) THEN change_qty WHEN change_type IN (2, 5) THEN change_qty ELSE change_qty END) AS ledger_qty FROM stock_ledger GROUP BY product_id ) l ON p.id l.product_id HAVING diff_qty 0;说明一下这条 SQL 的逻辑stock_ledger里所有的变动行都是带符号的入库为正、出库为负直接 SUM 就得到理论变化总量。左侧product的stock_quantity是每次过账累加的余额两者应当恒等。如果 diff 不为 0优先排查不是change_type3盘点调整漏写了流水就是某条过账在insertStockLedger之前就commit导致流水缺失。验证通过后做一件事对日常使用体验提升最大调整 JVM 启动参数。Swing 桌面程序默认堆内存 256MB 以内进销存界面组件多加上数据库连接池和缓存容易涨到阈值触发 Full GC 卡顿。在启动脚本java -jar后面加两个参数-Xms128m -Xmx512m。前者让 JVM 启动直接申请 128MB避免边跑边扩容后者限制上限防止内存泄漏拖垮整个系统。再加一个-Dfile.encodingUTF-8否则 Windows 中文环境下 Swing 字体和 SQL 参数里的中文会乱码这个坑在很多国产 Windows 服务器上几乎是必现的。还有一个改造方向是 Swing 带勾选框的树形菜单。原版 JTree 做菜单只能单选但真实业务里要给商品按分类批量选择比如「全部日用品类」自动勾选该分类下所有商品这时候需要给DefaultTreeCellRenderer定制一个带JCheckBox的渲染器和编辑器。实现思路不复杂渲染器继承DefaultTreeCellRenderer在getTreeCellRendererComponent里根据节点是否有子节点决定返回JCheckBox还是JLabel。这个功能做出来后大量采购场景的录入效率能提升一倍。我个人的血泪教训是改动 Swing 进销存代码前一定先做一次完整备份复制那台机器的数据库和源码目录。因为桌面程序不像 Web 服务有灰度发布改坏了一个JPanel布局用户那边全崩而且没有运维能帮你回滚。备份到另一个目录改出问题 10 秒内能恢复原样是唯一不会后悔药的事情。工具链上可以顺手集成一下 Maven 的shade插件把依赖打成一个 jar 后配合launch4j转成 Windows exe交付给不懂 Java 的客户双击即用比让客户配java -classpath按命令行跑专业得多。希望这篇笔记能帮你把这套 Swing 进销存源码吃透做课程设计能答辩顺利做项目交付能少熬几个改 bug 的夜。本文还有配套的精品资源点击获取
返回列表