简介:面向Java初学者的水果超市管理系统源码包,适合正在学习Java GUI、集合框架与JDBC数据库编程的读者。项目提供两种典型实现:一种基于ArrayList完成内存数据管理,另一种使用MySQL数据库并通过JDBC进行增删改查,帮助对照理解纯内存操作与持久化存储的差异。界面支持水果清单展示、添加、修改、删除等完整功能,代码结构清晰,便于模仿与二次开发。压缩包共41个文件,主要包含16个class字节码、15个java源码、2个依赖jar包(内含MySQL连接驱动),以及工程配置文件与界面图片,整体大小约704KB,解压后可直接导入IDE运行。同时附带的lib目录已备好驱动,省去额外配置的麻烦。目前已有3018人学习下载,适合课程设计或自学练手。
1. Java 水果超市管理系统:一套能直接跑的 Swing + JDBC 课设源码
一套以 Java + JDBC + Java GUI 组合完成的水果超市管理系统,从登录到收银的完整链路都能跑通。这类项目在课程设计和期末作业里出现频率很高,我最近拆过一套,核心就是 Swing 做界面、JDBC 连 MySQL、业务表围绕商品和订单展开。对刚开始接触 Java 桌面应用的人来说,这套源码的价值在于界面、数据库、业务逻辑三部分都能找到对应代码,不是只有一个空壳。对想快速上手的人来说,改改连接串和库表就能适配自己的环境。下面按我实际排查的顺序写,从功能拆解讲到避坑,最后给验证清单。
2. 系统功能拆解与技术选型:从登录到收银的完整业务链路
这套系统功能不复杂,但业务链路是完整的。用户登录、商品分类维护、水果信息录入、库存查询、前台收银、订单生成,这几个环节全部落在数据库表上。把它拆成模块列表看,才知道每一块代码该去哪里改。
| 模块 | 职责 | 涉及表 | 对应界面 |
|---|---|---|---|
| 登录与权限 | 校验员工账号,区分管理员与普通员工 | sys_user | 登录窗口 |
| 分类管理 | 增加、修改、删除水果分类 | fruit_category | 主界面分类页 |
| 商品管理 | 录入水果名称、品类、价格、库存 | fruit | 商品管理面板 |
| 销售收银 | 添加购物车、结算、扣减库存、生成订单 | sales_order、order_item | 收银面板 |
| 订单查询 | 按订单号或时间段查看销售记录 | sales_order、order_item | 订单查询面板 |
2.1 模块划分:四张表和三个界面撑起的业务闭环
从表结构来看,sys_user 管登录权限,fruit_category 和 fruit 管商品资料,sales_order 和 order_item 管销售流水。这里有个容易被忽略的点:fruit 表里存的 stock 库存字段,既要在商品管理页里被修改,又要在收银结算时被扣减,所以这个字段是业务核心,代码里要多处用到,改的时候要特别小心。
界面侧则按角色分流。登录进去后,普通员工看到收银和商品查询,管理员还能看到分类管理和订单管理。很多课设版本会把所有页面都开放给所有人,但如果你手里的源码已经做了权限判断,记得在 User 对象里保留 role 字段,因为后面所有界面控制都依赖它。
我一般拿到这样一套系统,会先把模块和表对应关系写出来,因为大多数报错不是 Java 语法问题,而是某个 DAO 方法对应的 SQL 写错了表名或字段名。比如 OrderDao 里插入订单时用了 order 做表名,而 order 在 MySQL 里是关键字,就会直接抛语法错误,这一点下面避坑章节会专门说。
2.2 技术选型:为什么是 Swing + JDBC 而不是 JavaFX + MyBatis
选题阶段很多人会纠结要不要用 JavaFX 加 MyBatis。我的看法是,这个组合对课设和入门来说性价比不高。Swing 是 JDK 自带的 GUI 框架,不需要额外引入依赖,打包和部署都省事;JDBC 是 Java 连数据库最基础的方式,每一个步骤都是显式的:拿连接、写 SQL、执行、关资源。这个过程虽然啰嗦,但对理解 Java 和数据库的交互非常有帮助。
MyBatis 的优势是 SQL 与代码分离,但它把连接管理、参数映射做了封装,反而让初学者看不懂底层。JavaFX 界面更现代,但 FXML、控制器、生命周期这些概念叠加起来,学习曲线一下子拉高了。对于一套以“跑通业务、展示增删改查”为核心目标的项目,Swing 加 JDBC 是最合适的方案,也符合大多数课程设计的技术要求。
如果你拿到手的资源里已经用了某种分包风格,不要急着大改。先跑通,再按自己的习惯调整,这是最稳妥的路径。
2.3 源码目录与运行入口:拿到压缩包先看哪里
常见的一套源码分包结构如下所示,不同版本可能略有差异,但入口类一般就在 com.fruit 或 src 根目录附近。
src/com/fruit/ ├── Main.java // 程序入口,启动主窗口 ├── db/ │ ├── DBUtil.java // JDBC 连接与关闭工具 ├── dao/ │ ├── UserDao.java // 登录校验 │ ├── FruitDao.java // 商品增删改查 │ ├── OrderDao.java // 订单与明细写入 ├── ui/ │ ├── LoginFrame.java // 登录窗口 │ ├── MainFrame.java // 主窗口,CardLayout 切换页面 │ ├── FruitPanel.java // 商品管理面板 │ └── SalePanel.java // 收银面板 └── sql/ └── init.sql // 建库建表脚本拿到资源后先做三件事:打开 sql 目录确认建表脚本存在;打开 db/DBUtil.java 确认数据库连接参数;打开 Main.java 确认启动入口。如果源码包里带了 lib 目录,直接把里面的 MySQL 驱动 jar 加入项目依赖;如果没有,就去 Maven 仓库下载对应版本的驱动,这一步决定了项目能不能在本地跑起来。
运行流程通常是先在 MySQL 里执行 init.sql,再修改 DBUtil 里的用户名密码,最后运行 Main。中间任何一个环节断了,都会出现后面要讲的驱动加载或连接失败问题。
3. 数据库设计与 JDBC 连接:建表语句、驱动参数与连接配置
数据库是整个项目能不能跑起来的地基。库表建错一个字段,Java 代码里就全是 SQL 异常。这套系统的表结构围绕“商品、订单、用户”三个核心对象展开,用四张表加上一张明细表就能支撑完整业务。
3.1 核心表结构:用户表、商品表、分类表、订单表
下面是基于这套业务最常见的建表脚本,我用 MySQL 8.0 语法写。如果你本机是 5.7,也兼容。
CREATE DATABASE fruit_mart DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE fruit_mart; CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT '1=管理员 0=员工', status TINYINT NOT NULL DEFAULT 1 COMMENT '1=可用 0=禁用' ); CREATE TABLE fruit_category ( id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(20) NOT NULL ); CREATE TABLE fruit ( id INT AUTO_INCREMENT PRIMARY KEY, fruit_name VARCHAR(50) NOT NULL, category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, unit VARCHAR(10) DEFAULT '斤', shelf_date DATE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_fruit_category FOREIGN KEY (category_id) REFERENCES fruit_category(id) ); CREATE TABLE sales_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(30) NOT NULL, staff_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_staff FOREIGN KEY (staff_id) REFERENCES sys_user(id) ); CREATE TABLE order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, fruit_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES sales_order(id), CONSTRAINT fk_item_fruit FOREIGN KEY (fruit_id) REFERENCES fruit(id) );建表脚本里有几个细节值得注意。数据库字符集用 utf8mb4,考虑到水果名称里可能有生僻字或表情符号,utf8mb4 比 utf8 更可靠。price 字段用 DECIMAL(10,2),不要用 FLOAT,因为金额计算有精度要求。sales_order 表不直接叫 order,因为 order 是 MySQL 的保留字,如果强行用 order 做表名,每次写 SQL 都要加反引号,纯属给自己挖坑。
3.2 JDBC 连接串:MySQL 8.0 实际踩过的参数
连接配置集中在 DBUtil 里,这是整个项目最容易翻车的地方。MySQL 8.0 的驱动类名和连接参数与 5.x 版本不一样,如果你手上是 8.0 的驱动,还写老的驱动类名,启动时直接报 ClassNotFoundException。
package com.fruit.db; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/fruit_mart" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL 驱动未找到,请确认 jar 已加入 classpath"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码里的参数逐个说清楚。serverTimezone=Asia/Shanghai 解决 MySQL 8.0 时区报错,不写这个会看到 The server time zone value 的异常。useSSL=false 是本地连接时避免 SSL 握手警告。allowPublicKeyRetrieval=true 解决 caching_sha2_password 插件下的公钥检索问题,这个参数在 MySQL 8.0 默认认证插件下几乎必加。字符集用 characterEncoding=utf8,与建库时的 utf8mb4 配合,避免中文乱码。
3.3 PreparedStatement 与资源关闭:账号验证代码的正确写法
登录查询如果直接拼字符串,虽然能跑,但一旦用户名里出现单引号,SQL 就崩了。更严重的是存在注入风险。正确做法是用 PreparedStatement,参数通过占位符传递。
public User login(String username, String password) { String sql = "SELECT id, username, 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, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRole(rs.getInt("role")); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }ps.setString 会把参数值按字符串类型安全地传给数据库,省去手动处理单引号的麻烦。查询条件里加 status = 1,表示只允许启用状态的账号登录,禁用账号直接拦在外面。try-with-resources 写法的好处是连接、语句、结果集都会自动关闭,不需要手写 finally,这在长方法里能避免漏关资源。
我第一次跑这套项目时,就是因为在 DBUtil 里少写了 allowPublicKeyRetrieval=true,本地连接一直报 Public Key Retrieval is not allowed,排查了半天才发现是参数问题,这个坑在下面专门整理。
4. GUI 与业务层分离:Swing 界面布局、事件监听与事务边界
Swing 本身不复杂,但很多人写出来的代码是界面和业务混在一起,按钮监听里直接写 SQL,导致后期改需求非常痛苦。这套源码里如果把 DAO 层单独拆出来了,界面和业务的边界就很清晰:UI 只负责收集输入、展示结果,DAO 负责和数据库打交道。
4.1 主窗体布局:JFrame、CardLayout 与面板切换
主界面一般用一个 JFrame 装多个 JPanel,用 CardLayout 做页面切换。登录成功后从登录面板切到主面板,退出登录再切回来,这种模式在课程设计里很常见。
JFrame frame = new JFrame("水果超市管理系统"); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setSize(1024, 640); frame.setLocationRelativeTo(null); CardLayout cardLayout = new CardLayout(); JPanel container = new JPanel(cardLayout); LoginPanel loginPanel = new LoginPanel(container); MainPanel mainPanel = new MainPanel(); container.add(loginPanel, "login"); container.add(mainPanel, "main"); cardLayout.show(container, "login"); frame.setContentPane(container); frame.setVisible(true);把 container 传给 LoginPanel,是为了在登录成功后让登录面板能直接调用 cardLayout.show(container, "main")。这种做法比在 Main 类里写一堆切换逻辑要清爽。setLocationRelativeTo(null) 让窗口居中显示,setSize(1024, 640) 是给普通笔记本屏幕留出足够空间,如果改成 800x600 也能跑,就是表格区域会挤一点。
CardLayout 切换时不会销毁页面,切回主面板时之前填过的表单内容还在。如果希望在每次进入某个页面时刷新数据,可以在 show 之后手动调用对应面板的加载方法。
4.2 登录模块:事件监听与数据库校验解耦
登录按钮的监听器里不要直接写 JDBC 代码,而是调用 UserDao 的方法,拿到结果后再决定切换页面还是弹错误提示。
loginBtn.addActionListener(e -> { String name = userText.getText().trim(); String pwd = new String(pwdField.getPassword()); User user = userDao.login(name, pwd); if (user != null) { currentUserLabel.setText(user.getUsername() + " 已登录"); cardLayout.show(container, "main"); } else { JOptionPane.showMessageDialog(this, "账号或密码错误"); } });密码框用 getPassword() 拿到的是 char[] 而不是 String,这是 Swing 的一个设计细节,目的是减少密码字符串在内存中被复用的风险。如果业务上需要 String,再用 new String(...) 转换。userText.getText().trim() 去掉首尾空格,避免用户不小心输入空格导致登录失败。
登录成功后的 user 对象可以传给主面板,后续收银时记录操作员就用这个对象里的 id。如果主面板拿不到 user,可以临时存在一个全局的 Session 类里,但最简单的方式是在构造 MainPanel 时把 user 传进去。这套源码里如果已经写了类似 CurrentUser 的持有类,直接用就行。
4.3 订单结算:事务开启、库存扣减与回滚
收银是系统里唯一涉及多表写入的操作:要插入订单主表、插入订单明细、扣减商品库存。这三个操作必须在一个事务里完成,任何一个失败都要全部回滚,否则会出现订单生成但库存没扣的脏数据。
public void createOrder(Order order, List<OrderItem> items) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); String orderSql = "INSERT INTO sales_order(order_no, staff_id, total_amount) VALUES(?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, order.getOrderNo()); ps.setInt(2, order.getStaffId()); ps.setBigDecimal(3, order.getTotalAmount()); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { order.setId(keys.getInt(1)); } } } String itemSql = "INSERT INTO order_item(order_id, fruit_id, quantity, price) VALUES(?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(itemSql)) { for (OrderItem item : items) { ps.setInt(1, order.getId()); ps.setInt(2, item.getFruitId()); ps.setInt(3, item.getQuantity()); ps.setBigDecimal(4, item.getPrice()); ps.addBatch(); } ps.executeBatch(); } String stockSql = "UPDATE fruit SET stock = stock - ? WHERE id = ? AND stock >= ?"; try (PreparedStatement ps = conn.prepareStatement(stockSql)) { for (OrderItem item : items) { ps.setInt(1, item.getQuantity()); ps.setInt(2, item.getFruitId()); ps.setInt(3, item.getQuantity()); ps.executeUpdate(); } } conn.commit(); } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }事务代码的关键点有两处。一是 conn.setAutoCommit(false) 必须在所有写入操作之前调用,否则每条 SQL 都会自动提交,回滚就失效了。二是库存扣减语句里写了 stock >= ?,这一条件天然防止库存扣成负数。如果库存不足,这条更新语句影响行数为 0,你可以在 executeUpdate 后检查返回值,小于 1 就抛异常触发回滚。
executeBatch 的作用是批量插入明细,比起循环里逐条 executeUpdate,性能和代码可读性都好很多。finally 里把 autoCommit 恢复成 true 是给连接池场景用的,否则连接归还后还保持手动提交状态,会影响下一次使用。
5. JDBC 与 Swing 避坑指南:五个高频问题与排查路径
这套源码整体跑通不难,但我在帮人排查时反复遇到过几个固定问题,现象几乎一样,原因却各不相同。下面按“现象、原因、解决”的顺序整理,照着查能省不少时间。
5.1 驱动加载时直接报 ClassNotFoundException
现象:运行 Main 类,控制台第一行就抛出 ClassNotFoundException,指向的类名是 com.mysql.jdbc.Driver。原因:项目里没有引入 MySQL 驱动 jar,或者驱动 jar 版本与代码里的驱动类名不匹配。MySQL 8.0 的驱动类改成 com.mysql.cj.jdbc.Driver,老写法 com.mysql.jdbc.Driver 虽然在 5.x 驱动里能用,但在新驱动里已经移除了。解决:先确认 MySQL 版本,再检查项目 classpath 里有没有 mysql-connector-java 的 jar。如果是 Maven 项目,检查 pom.xml 里依赖是否存在。最后确认 DBUtil 里的 DRIVER 常量与依赖版本一致。
5.2 java.util.Date 转 java.sql.Date 存不进去
现象:往 fruit 表的 shelf_date 字段插入日期时,要么报错,要么存进去的时间不对,通常是少了时分秒。原因:JDBC 的 setDate 方法只接受 java.sql.Date,代码里如果直接传 java.util.Date,编译不会报错,运行时会提示类型不匹配。解决:在 DAO 层做一次转换,把 java.util.Date 转成 java.sql.Date,常见写法是 new java.sql.Date(utilDate.getTime())。如果你只需要日期部分,用这个转换就够了;如果需要完整时间,应该用 setTimestamp 配合 java.sql.Timestamp。
5.3 点击按钮后界面白屏卡死,过几秒才恢复
现象:在登录或收银时点击按钮,窗口立刻变白,鼠标转圈,感觉整个程序假死。原因:按钮事件默认跑在 Swing 的事件分发线程上,也就是 EDT。DAO 查询数据库、等待响应这些耗时操作如果直接写在监听器里,EDT 被占住,界面就无法重绘。解决:把耗时查询放到新线程里,SwingWorker 是官方推荐的做法。查询完成后用 SwingUtilities.invokeLater 把结果回写到组件上。不要直接在后台线程里操作 JTable 或 JLabel,Swing 组件不是线程安全的。
5.4 中文乱码:数据库里全是问号
现象:界面输入框里输入中文正常,点保存后去数据库看,字段值变成了一串问号。原因:数据库、连接串、页面编码三方不一致。常见组合是库表是 latin1 或 gbk,连接串没带 characterEncoding,导致字符集转换失败。解决:建库时统一用 utf8mb4,连接串加上 useUnicode=true&characterEncoding=utf8,这两步做完基本不会再乱码。如果还乱码,检查 IDE 控制台的编码设置,Windows 下把运行环境改成 -Dfile.encoding=UTF-8。
5.5 Connection 没关导致连接耗尽
现象:项目跑一段时间后,报错 Too many connections,重启后恢复,过一会儿又报同样的错。原因:登录或查询操作拿到的 Connection 没有关闭。每调用一次 getConnection 就占用一个连接,连接池里的连接被耗尽,后续请求全部阻塞。解决:所有 JDBC 操作都用 try-with-resources 包裹,确保 Connection、PreparedStatement、ResultSet 全部自动关闭。如果你拿到手的源码里还在用老式写法,只在 finally 里关了 Connection 但没关 Statement 和 ResultSet,也要一起补上。
6. 验证与进阶:从“能跑通”到“改得动”的四个动作
先给一套验证清单,确保系统在你的机器上能正常跑通。按顺序执行,每完成一步就对照预期结果,及时发现问题。
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 在 MySQL 中执行 init.sql | 建库建表成功,无报错 |
| 2 | 修改 DBUtil 中的用户名和密码 | 程序启动后不报连接错误 |
| 3 | 运行 Main.java | 弹出登录窗口 |
| 4 | 使用 init.sql 里初始化的账号登录 | 进入主界面,右上角显示当前用户 |
| 5 | 在商品管理页新增一条水果记录 | 表格新增记录,数据库可查到 |
| 6 | 在收银页添加商品并结算 | 库存减少,订单表和明细表各多一条数据 |
| 7 | 查询订单列表 | 能看到刚生成的订单,金额与明细一致 |
跑通之后,如果想在源码基础上继续进阶,我建议先做两个改动。第一个是把数据库连接参数抽到配置文件里。现在 DBUtil 里的 URL、用户名、密码都是硬编码的,换一台电脑就得改代码重编译。建一个 db.properties 文件放在 resources 目录下,再用 Properties 类读取,后续只改文件不动代码。
Properties props = new Properties(); try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { props.load(in); }第二个改动是加一个导出 CSV 的功能。水果超市的经营者会想要把商品列表导出成表格,这个功能实现成本很低,但很实用。从表格模型里取出 JTable 的每一行数据,按 CSV 格式写入文件即可。注意字段内容里如果有逗号,要用引号包起来,否则用 Excel 打开时列会错位。
try (FileWriter fw = new FileWriter("fruits.csv"); BufferedWriter bw = new BufferedWriter(fw)) { bw.write("水果名称,分类,价格,库存"); bw.newLine(); for (String[] row : tableData) { bw.write(String.join(",", row)); bw.newLine(); } }有一次我把连接串里的 serverTimezone 去掉,结果换到另一台电脑跑直接报时区错误,排查了大半天才发现是参数变更导致的。从那以后,只要是从别人手里接过来的 JDBC 项目,我都强制先打开 DBUtil 和 init.sql 确认一遍参数再启动,这个习惯帮我省了太多冤枉时间。希望帮到你。
本文还有配套的精品资源,点击获取