ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java+Swing+MySQL超市商品管理系统:JDBC到JTable完整实战

Java+Swing+MySQL超市商品管理系统:JDBC到JTable完整实战

简介:基于Java+Swing+MySQL开发的超市商品管理系统完整源码包,面向Java初学者、数据库课程设计及毕业设计场景。系统围绕商品、库存、供应商三大核心表展开,通过Swing界面完成商品添加、查询、修改、删除等CRUD操作,并结合MySQL实现数据持久化;代码中包含事务回滚、异常处理及用户名密码登录等安全机制,展示了桌面应用从界面设计到数据库操作的整体流程。压缩包内共46个文件,含28个class编译文件、11个java源码文件、2个jar依赖库、1个SQL建表脚本及工程配置文件,整体仅1.59MB,目录结构清晰,方便导入IDE直接运行。目前已有2305人学习下载,源码、数据库脚本及项目配置完整,适合借鉴学习Swing布局管理、JDBC编程、事务控制与基础安全策略等知识。

1. Java+Swing+MySQL超市商品管理系统:一个“老技术”为什么还值得做一遍

很多人看到“Java+Swing+Mysql实现超市商品管理系统源码”这串词,第一反应是“又是课设代码”“太老了没价值”。但真正动手做过一遍就会发现:这个组合把 JDBC 驱动加载、数据库连接、DAO 封装、Swing 事件模型、JTable 数据刷新、打包交付整条链路全部串了起来,恰好是一份完整的“Java 桌面应用最小工程样本”。它适合两类人——想用 Java 做人生第一个完整系统的初学者,以及要交课程设计但又不想拿网上零散代码拼凑的开发者。超市日常的入库、改价、盘点、销售记录,都可以被这套单机系统覆盖;只有把增删改查真正跑通,你才会理解“源码”里真正值钱的部分并不是那几张表,而是数据如何安全地在 MySQL 和界面之间流转。

2. 选型逻辑与工程骨架:为什么是 Swing 和 MySQL

2.1 为什么是 Swing + 原生 JDBC,而不是 JavaFX 或 MyBatis

先想清楚一个前提:这个系统服务的是一家超市门店的日间营业场景,使用者是店员和店长,数据量级在几千到几万条商品记录,不要求多人在线并发操作。“单机部署、双击即用、几天内交付”才是刚需,而不是微服务或前后端分离。

Swing 在这个场景里有三个无法被替代的优势:第一,它是 JDK 自带的 GUI 框架,不需要额外运行时,打出的 jar 包在装了 Java 的电脑上就能跑;第二,Swing 的 MVC 结构松散,可以用 JTable 直接绑定业务数据,改起来比想象中顺手;第三,Swing 虽然没有现代前端框架那么漂亮,但对于后台录入型界面,它的布局可控性完全够用。

相比之下,JavaFX 的动画和样式更现代,但需要额外引入运行时库,混乱的 fxml 和控制器关系对新手并不友好;而 MyBatis 这类 ORM 框架虽然能减少 SQL 编写量,但小系统引入它反而要处理 XML 映射、插件配置、日志依赖,出错时排查链路更长。原生 JDBC 的最大“代价”是代码量大一些,但换来的是每一行 SQL 都在你掌控范围内,这对理解系统运行原理最有利。

方案部署方式学习曲线适合场景
Swing + JDBC双击 jar / exe低单机桌面录入、后台管理
JavaFX + MyBatis需 JavaFX 运行时中高界面要求更高的桌面应用
Web 后台系统需应用服务器中等多人远程访问、数据集中管理

所以,这个标题选 Swing + MySQL 不是技术落后,而是在“能解决的问题”和“付出的学习成本”之间取了一个平衡点。你如果后续想把系统改成 Web 版,DAO 层和数据库设计依然可以直接迁移,这也是我推荐先做桌面版的原因之一。

2.2 工程骨架:包结构决定你后面少走多少弯路

做这种系统最容易犯的错,是打开 IDE 就写登录界面,把 SQL 直接写在按钮事件里。界面逻辑和数据库访问搅在一起后,你会发现自己连“改一个查询条件”都要翻半天的界面代码。我通常第一步先搭包结构,把职责隔离好。

一个顺手的分包方案如下:

src/main/java ├─ ui // 登录窗口、主窗口、商品管理面板 ├─ service // 登录校验、库存变更等业务逻辑 ├─ dao // GoodsDao、UserDao,只写 SQL 和结果集映射 ├─ model // Goods、User 实体类 └─ util // DBUtil 数据库连接、HashUtil 加密

model 层就是普通的 POJO,字段和表结构一一对应;dao 层只负责“把 SQL 发出去,把 ResultSet 变成对象”;service 层处理“库存够不够、状态是否可用”这类业务判断;ui 层只做两件事——收集用户输入、调用 service 并展示结果。这样分层之后,测试数据库连接不需要打开界面,替换查询逻辑不会碰按钮事件,是实战里最省钱的做法。

2.3 第一个要写好的文件:DBUtil 连接工具类

数据库连接是整个系统的命门。我把 DBUtil 放在所有代码的最前面写,因为后续每个 DAO 方法都要依赖它。这里用的是 JDBC 原生连接,配置常量放类里,先保证能跑通,再改成读取外部配置文件。

package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/supermarket" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动加载失败,请检查依赖", e); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (Exception ignored) {} try { if (stmt != null) stmt.close(); } catch (Exception ignored) {} try { if (conn != null) conn.close(); } catch (Exception ignored) {} } }

这段代码有三个参数需要仔细说。URL 里的useSSL=false是关闭 SSL 握手协议,本地开发环境不关的话,部分 MySQL 版本会反复提示,换个版本甚至直接连接失败;serverTimezone=Asia/Shanghai是因为新版 JDBC 驱动要求指定时区,不设就会报“Server returns invalid timezone”错误;characterEncoding=UTF-8是保证中文写入数据库不乱码的关键,很多“中文变问号”的故障其实就少写了这个参数。

驱动类com.mysql.cj.jdbc.Driver对应 MySQL 5.7 及 8.x 版本,老项目里常见的com.mysql.jdbc.Driver在新驱动里已经废弃,如果你用的是 5.1.x 系列驱动,要保留旧写法。close方法的用处是兜底释放资源,但后面我们用 try-with-resources 就不需要手动调用它了。

提示:不要把数据库密码写死在代码里,项目做完前把配置挪到 config.properties 中,用 java.util.Properties 读取,方便不同机器上部署时互不影响。

3. 数据库设计与初始化:商品表不是“id+name”这么简单

3.1 建库建表:字段类型选的草率,后面返工的是全部代码

数据库是系统的地基。我见过最典型的翻车现场:价格列用 FLOAT,跑了一个月后统计金额差了三分钱;库存列用了无符号 INT,退货时一减就报错。这些坑在表设计阶段就能避免,不需要等代码报错才发现。

商品表t_goods是系统的核心,建议先用下面这份 SQL 建库建表:

CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; CREATE TABLE t_goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '商品名称', category VARCHAR(50) NOT NULL DEFAULT '未分类' COMMENT '商品分类', price DECIMAL(10,2) NOT NULL COMMENT '零售价,单位元', stock INT NOT NULL DEFAULT 0 COMMENT '当前库存量', status TINYINT NOT NULL DEFAULT 1 COMMENT '1 在售,0 下架', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', INDEX idx_goods_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段类型选择的四个原则,我做项目时基本都会这样约定:金额一律用DECIMAL(10,2),绝不使用 FLOAT 或 DOUBLE,因为二进制浮点数无法精确表示小数,累计求和会产生误差;库存数量用普通INT,不要加UNSIGNED,否则退货减库存时一旦算错直接报“Out of range”错误,排查起来很痛苦;状态字段用TINYINT而不用 VARCHAR,因为'1'、'0'的存储和索引效率远高于'在售'、'下架',界面显示时再做映射;创建时间交给数据库默认值生成,比在 Java 代码里传时间更可靠,避免不同电脑系统时间不一致的问题。

id用自增主键是单机系统的默认选择。索引方面,name字段加了普通索引,商品搜索基本都按名称条件查询,这里能显著减少全表扫描;但如果页面要求支持模糊查询LIKE '%可乐%',索引会失效,需要在搜索功能设计时注意,这一条放到后面避坑章细讲。

3.2 用户表与初始数据:登录校验的最小闭环

超市商品管理系统的登录功能比较简单,但表结构不能随便。用户表要包含状态字段,不然账号被禁用时你只能在代码里写死判断。建表语句如下:

CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(72) NOT NULL COMMENT '加盐哈希后的密码', status TINYINT NOT NULL DEFAULT 1 COMMENT '1 可用,0 禁用', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为了先把登录流程跑通,初始化数据阶段可以先用一个明文占位密码,比如'123456',等登录取数成功后再升级为 SHA-256 盐值哈希。常见做法是在 util 包下加一个 HashUtil,登录时先加固定盐再哈希,数据库中存储哈希值而不是明文。为什么强调这一步?很多人的源码能跑,但密码直接裸奔,这种系统不要说给客户,就是拿去交课设也容易被追问到答不上来。

验证最小闭环需要两条数据,一个管理员账号、一个测试商品:

INSERT INTO t_user (username, password, status) VALUES ('admin', '123456', 1); INSERT INTO t_goods (name, category, price, stock, status) VALUES ('可乐', '饮料', 3.50, 100, 1);

插入后检查一下自增 ID 和中文显示是否正常。如果是从 Windows 命令行执行 SQL,要先执行chcp 65001或用 UTF-8 连接;这一步经常被忽略,导致后续所有中文数据在界面上显示乱码,其实源头在插入阶段就已经错乱了。

3.3 验证表结构:三个查询就能发现八成设计问题

建表完成后不要急着写界面,先用三条 SQL 把表结构验证一遍,这种习惯能省下后面一晚上的排错时间。

第一条,确认商品数据量:

SELECT COUNT(*) FROM t_goods;

如果数量对不上预期,多半是插入时事务没有提交,或者连接到了错误的数据库实例。检查连接 URL 中的库名是否拼写正确。

第二条,确认价格精度:

SELECT name, price, price * 0.9 AS discount_price FROM t_goods;

打印出的打折价如果出现 3.5000000000000004 这种尾巴,说明类型有问题;正常应该显示 3.15。DECIMAL会在数据库层保证精度,但如果你后续在 Java 代码里做了运算,还要注意BigDecimal的使用方式。

第三条,验证用户名查询是否命中唯一索引:

EXPLAIN SELECT * FROM t_user WHERE username = 'admin';

看到type为const或ref,说明索引生效,登录查询不会拖慢系统。如果显示ALL全表扫描,就要检查UNIQUE约束是否真的加上去了。

4. 商品管理核心功能实现:从 DAO 到 JTable 的增删改查闭环

4.1 DAO 层查询与新增:SQL 写在哪、事务从哪开始

DAO 层只做数据访问,不做业务判断。查询商品列表时最安全的做法是每次根据条件拼接 SQL 而不是把整个表拉到内存里;对于单机小系统,列表全量加载问题不大,但超过 5000 条记录后界面会有明显卡顿。建议先写一个通用查询方法,再用条件查询扩展:

package dao; import model.Goods; import util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class GoodsDao { public List<Goods> findAll() throws Exception { String sql = "SELECT id, name, category, price, stock, status FROM t_goods ORDER BY id DESC"; List<Goods> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Goods g = new Goods(); g.setId(rs.getInt("id")); g.setName(rs.getString("name")); g.setCategory(rs.getString("category")); g.setPrice(rs.getBigDecimal("price")); g.setStock(rs.getInt("stock")); g.setStatus(rs.getByte("status")); list.add(g); } } return list; } }

这里使用 try-with-resources 而不是手动关连接,代码更简洁,也不容易漏掉资源释放。价格字段用getBigDecimal而不是getDouble,是因为数据库层是DECIMAL(10,2),用浮点数接收会再次引入精度问题。

新增商品时要拿到自增主键,但我见很多人写的 insert 方法不取主键,导致后面“修改商品”功能只能按名称匹配,重复商品名一出现就出错。正确写法是用Statement.RETURN_GENERATED_KEYS:

public int insert(Goods goods) throws Exception { String sql = "INSERT INTO t_goods(name, category, price, stock, status) VALUES(?, ?, ?, ?, 1)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, goods.getName()); ps.setString(2, goods.getCategory()); ps.setBigDecimal(3, goods.getPrice()); ps.setInt(4, goods.getStock()); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { return keys.getInt(1); } } } return -1; }

事务边界我一般放在 service 层管理。比如“新增商品同时记录一条入库日志”,两个操作必须同时成功或失败,这是就需要关闭自动提交,把多个 DAO 方法包在同一个事务里。常见的错误写法是把事务控制放在 DAO 内,这会导致组合业务无法统一回滚。新手阶段,单表插入不需要事务,但在做“一次操作写多张表”的功能时必须先想清楚事务边界。

4.2 JTable 刷新与添加商品事件:界面和业务怎么分工

Swing 界面部分最核心的组件是 JTable,商品列表的展示和操作都要围绕它展开。主窗口里创建一个 JTable,数据模型用 DefaultTableModel,并提供列名数组:

String[] columns = {"ID", "名称", "分类", "价格", "库存", "状态"}; DefaultTableModel tableModel = new DefaultTableModel(columns, 0); JTable goodsTable = new JTable(tableModel);

刷新列表的正确姿势是先清空模型再往里填数据,而不是用addRow追加。因为 JTable 在数据刷新后不会自动清理旧行,你只调用一次findAll直接addRow,第二次刷新时旧的 100 行下面又叠加了新的 100 行,表格越点越多,这就是传说中的“幽灵行”。

private void refreshTable() { DefaultTableModel model = (DefaultTableModel) goodsTable.getModel(); model.setRowCount(0); try { for (Goods g : goodsDao.findAll()) { model.addRow(new Object[]{ g.getId(), g.getName(), g.getCategory(), g.getPrice().setScale(2, java.math.RoundingMode.HALF_UP), g.getStock(), g.getStatus() == 1 ? "在售" : "下架" }); } } catch (Exception ex) { JOptionPane.showMessageDialog(this, "加载商品失败:" + ex.getMessage()); } }

价格输出前用setScale(2, RoundingMode.HALF_UP)做格式化,避免出现3.5和3.50混排;状态字段显示成中文文案,比直接显示1、0对门店店员更友好。setRowCount(0)是每次刷新时最保险的一行代码,我习惯在任何刷新逻辑第一行都加上。

添加商品按钮的事件写法要注意两点——输入校验放在界面层还是业务层。我的约定是“格式校验放界面层,业务校验放 service 层”。比如价格是不是数字,这是格式问题,可以在按钮事件里先判断;但库存是否超过预警值,这是业务规则,必须放到 service 层做,否则别人换一个界面就能绕过你的校验逻辑:

addButton.addActionListener(e -> { String name = nameField.getText().trim(); if (name.isEmpty()) { JOptionPane.showMessageDialog(this, "商品名称不能为空"); return; } BigDecimal price; try { price = new BigDecimal(priceField.getText().trim()); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, "价格必须是数字"); return; } if (price.compareTo(BigDecimal.ZERO) < 0) { JOptionPane.showMessageDialog(this, "价格不能为负数"); return; } Goods g = new Goods(); g.setName(name); g.setCategory(categoryField.getText().trim()); g.setPrice(price); g.setStock(Integer.parseInt(stockField.getText().trim())); try { goodsDao.insert(g); refreshTable(); nameField.setText(""); priceField.setText(""); stockField.setText(""); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "保存失败:" + ex.getMessage()); } });

4.3 库存与状态变更:把业务规则放进 Service 层

库存字段直接暴露给 DAO 是不合适的,因为“卖出扣减库存”这个动作不应该被界面直接操作数据库。更安全的做法是在 service 层写一个专门的方法,负责判断库存是否充足,并在同一事务里完成扣减:

package service; public class GoodsService { private final GoodsDao goodsDao = new GoodsDao(); public boolean reduceStock(int goodsId, int count) throws Exception { if (count <= 0) { throw new IllegalArgumentException("扣减数量必须为正数"); } Goods goods = goodsDao.findById(goodsId); if (goods == null) { throw new RuntimeException("商品不存在,ID=" + goodsId); } if (goods.getStock() < count) { return false; // 库存不足,不下发 SQL 执行 } goodsDao.updateStock(goodsId, goods.getStock() - count); return true; } }

扣减前的“先查库存再更新”存在一个多线程窗口问题,单机桌面系统里同一时间只有一个操作者,基本可以忽略;但如果未来接入收银端多台机器同时操作,就必须改成UPDATE ... WHERE stock >= ?原子扣减语句,否则会出现超卖。这一点是熟手和新手的明显区别:做底层 DAO 的人只管执行 SQL,做业务层的人必须考虑并发边界在哪里。

这里的updateStock对应一条简单的更新语句,更新成功后再返回布尔值。库存变更虽然有窗口期问题,但在单机桌面场景下仍然是清晰、可读性最好的写法,先把业务正确性做对,再谈并发优化,是这类系统该有的推进顺序。

5. 避坑排查:5 个让 Swing+MySQL 项目翻车的常见问题

5.1 中文全变问号:从数据库到界面全乱

现象:JTable 里显示的商品名是???,或者数据库里插入成功但命令行 SELECT 出来也是问号。

原因:这通常是三层中的某一层用了非 UTF-8 编码。最常见的是 JDBC URL 少了characterEncoding=UTF-8;其次是 Linux 或 macOS 下 MySQL 服务端默认字符集不是 utf8mb4,建表时DEFAULT CHARSET=utf8mb4被漏掉;还有少部分情况是项目源码文件本身存成了 GBK,IDE 编译时没转成 UTF-8。

解决:第一,在 JDBC URL 中固定补全characterEncoding=UTF-8&useUnicode=true;第二,建表语句显式指定DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;第三,检查开发环境的文件编码设置,确保源码文件也是 UTF-8。这三步都做对之后,中文乱码基本不会再出现。

5.2 找不到驱动类或连接超时:版本与 classpath 问题

现象:程序一启动就抛ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者运行时Communications link failure。

原因:前者是 jar 包没放到 classpath 里,后者多半是 URL 写法或 TCP 端口不通。很多人在 IDE 里能跑,但打包成可执行 jar 后又报找不到驱动,这是因为 IDE 自动加载了依赖,jar 内部却没有 lib 目录。

解决:开发阶段用构建工具引入 mysql-connector-j 依赖,并确认 scope 不是 compile-only;如果手动导入 jar,放在项目根目录的 lib 文件夹下并 Add as Library。打包时保证 jar 包同级有 lib 目录,里面放着驱动包,清单文件里的Class-Path正确指向它。测试连接阶段,可以用telnet 127.0.0.1 3306确认 MySQL 端口是否监听得通,排除防火墙干扰。

5.3 修改/删除后表格“幽灵行”不消失

现象:第一次打开列表正常,点击“刷新”按钮后表格下方多出重复的记录,行数翻倍增长。

原因:这段代码里刷新逻辑写成model.addRow(...)但不先清空旧行,或者只重置了列数据而没有调用setRowCount(0)。JTable 的数据模型是“增量追加”的,不清空模型就意味着旧数据永远留在里面。

解决:把刷新方法的第一行固定写成model.setRowCount(0),在随后循环中重新 addRow。更稳妥的办法是每次刷新时直接给DefaultTableModel设置新的二维数组,彻底避免“残留行”的存在。这条规则可以推广到所有 JTable、JComboBox 的动态刷新场景。

5.4 增删改后拿不到自增主键 ID

现象:新增商品后需要立刻用返回的主键继续做订单表插入,但拿到的永远是 -1,或者插入后列表里主键顺序不直观。

原因:JDBC 在executeUpdate后默认不会自动回填自增键,单纯执行ps.getGeneratedKeys()前如果没有指定Statement.RETURN_GENERATED_KEYS,返回的 ResultSet 是空的。

解决:在prepareStatement时把第二个参数传Statement.RETURN_GENERATED_KEYS,执行完executeUpdate后再调用getGeneratedKeys()。另外注意,executeUpdate()方法最终返回的影响行数,不要误把它当成主键值。

5.5 窗口一打开就卡死:Swing 单线程模型的代价

现象:点击“查询”按钮后整个窗口转圈,点哪都没反应,几分钟后一次性弹出一堆提示框。

原因:Swing 所有界面操作都必须跑在事件分发线程 EDT 上,耗时操作也放在里面就会阻塞界面。商品数量超过万行时,findAll()的查询和 JTable 渲染都堆在 EDT 里,用户界面就“死了”。

解决:耗时操作放到SwingWorker或单独线程中执行,拿到结果后再通过invokeLater切回 EDT 更新界面。单机管理系统最常见的妥协是:保留一个“数据量小、加载快”的前提,并用分页查询把单次结果控制在 500 行以内。我一般建议先做分页,因为分页是必须的,多线程渲染可以等系统真正卡了再考虑,避免过度设计。

6. 从能跑到能交付:打包、导出 Excel 与一天验收

6.1 打可执行 jar:把 MySQL 驱动放在 lib 下而不是打进包里

系统开发完成后,最大的坑是把驱动 jar 打包进主 jar 内部。这样做的后果是当你通过 IDE 运行时一切正常,一旦双击supermarket.jar,瞬间报ClassNotFoundException。推荐的做法是主 jar 和 lib 目录同级,通过清单文件指定依赖路径。建立MANIFEST.MF配置:

Manifest-Version: 1.0 Main-Class: ui.LoginFrame Class-Path: lib/mysql-connector-j.jar

把主程序和 lib 放在同一层目录后整体压缩,交付时就是这样一个结构:

超市管理系统/ ├─ supermarket.jar └─ lib/ └─ mysql-connector-j.jar

这样做的好处是更换 MySQL 驱动版本时只需要替换 lib 里的文件,不需要重新打包主程序。

6.2 用 Apache POI 导出 Excel:给门店留一张可打印的报表

虽然系统叫“商品管理”,但门店店长几乎都会要求“把库存表导出成 Excel,我要对账”。在 JTable 已有数据的情况下,用 Apache POI 的XSSFWorkbook能快速生成.xlsx文件。思路是遍历DefaultTableModel,把每行数据写入 Excel 的 Row 和 Cell,最终输出到用户选择的位置。核心示例片段:

Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("商品列表"); for (int i = 0; i < tableModel.getRowCount(); i++) { Row row = sheet.createRow(i); for (int j = 0; j < tableModel.getColumnCount(); j++) { row.createCell(j).setCellValue(String.valueOf(tableModel.getValueAt(i, j))); } } try (FileOutputStream out = new FileOutputStream(file)) { workbook.write(out); }

这一步是在为“交付验收”做准备。导出功能不实现,系统总给人一种“还差最后一环”的半成品观感。

6.3 验收:按真实业务顺序跑一遍你的系统

最后,花一整天模拟真实开店流程来验收:从登录开始,新增 20 个商品、多次修改价格、下单扣减库存、下架一批商品、导出 Excel 报表,全程注意操作后的数据一致性。用这个顺序走一遍比写 100 行代码更能发现问题。

我最早做这类系统时,把时间都花在调表格样式上,结果验收前才发现导出功能没有做,差点交不了差。记住:先跑通闭环,再想界面漂亮,功能交付的顺序才是这个项目里最大的坑。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进