本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:面向高校Java课程设计的桌面端水果摊进销存系统,基于Swing开发图形界面,支持商品管理、库存实时查询、进货/销售记录登记、员工信息维护等实用功能。后端使用MySQL 8.0数据库,压缩包内附fruit.sql建表及初始化数据脚本,可直接在Navicat中导入并执行可视化操作,包括连接配置、表结构浏览、CRUD操作和简易图表分析,方便教学演示与作业验收。源码结构清晰,严格遵循分层设计(entity/dao/views/util),含全部.java源文件、编译后的.class文件、MySQL JDBC驱动jar包、UML类图(diagram.uml)、UI配置文件及详细开发文档(Word格式),涵盖需求说明、系统架构、模块划分、运行环境配置(JDK 8 + Windows验证通过)、关键界面截图与启动方式(主类Fruit)。所有代码无加密、无混淆,注释完整,适合作为课程大作业参考、自学练手项目或二次开发基础模板。

1. 项目概述:这不是一个“玩具系统”,而是一套能真正跑起来的课程级进销存骨架

你手头拿到的这个“Java Swing水果摊管理系统”,绝不是那种只在PPT里画得漂亮、运行起来就报错十几次的“教学演示工程”。它是我带过三届Java课程设计后,把学生踩过的所有坑、老师验收时最常问的五个问题、以及期末答辩最容易卡壳的三个模块,全部反向揉进代码和文档里的产物。关键词里写的“Java课程设计”“Swing桌面应用”“MySQL进销存”,每一个都不是虚词——它对应着真实课堂场景里的刚性需求:学生要能在3天内配好环境跑通主界面,7天内看懂DAO层怎么跟数据库对话,10天内能独立改出“按日期查销售总额”的新功能;老师要能用Navicat点几下就验证数据是否真实写入,截图就能当评分依据;而课程组后续想把它升级成带报表导出的版本,也不用推倒重来。

我特别强调“能真正跑起来”,是因为见过太多所谓“完整源码包”,解压后第一眼看到src目录就心凉半截:entity类里字段全是String name没加注释,DAO方法名叫getInfo()却不知道查的是员工还是水果,views包里一个MainFrame.java塞了800行事件监听器。这套系统不是这样。它的分层是呼吸感十足的:entity里每个类顶部都有Javadoc说明业务含义(比如Fruit.java第一行写着“【实体】水果商品,id为自增主键,unit字段仅接受’kg’或’件’,用于库存计算精度控制”);dao里每个接口方法名直指意图(FruitDao.insertBatch(List<Fruit> fruits)而不是saveAll());views下的每个JPanel都只负责一件事(StockQueryPanel只做库存查询,不碰进货逻辑)。就连那个看似不起眼的util包,也藏着DateUtils.formatDateForDB(LocalDate.now())这种把LocalDate转成"2025-04-12"字符串的工具方法——因为MySQL的DATE类型不吃2025/04/12这种格式,学生第一次连不上数据库,八成栽在这儿。

压缩包里那个开发文档 java大作业-水果摊信息管理系统.docx,也不是应付差事的模板。它第一页就用加粗红字标着:“运行前必读:Windows下若双击Fruit.bat无反应,请右键→编辑,检查JAVA_HOME路径是否指向JDK 8安装目录(如C:\Program Files\Java\jdk1.8.0_291),而非JRE”。这种细节,只有真在机房守着学生调试到凌晨的人才写得出来。而fruit.sql脚本更狠——它开头就注释着:“– 注意:此脚本默认创建fruit_db数据库,并授权给’root’@’localhost’,若你的MySQL用户名不是root,请手动修改第5行GRANT语句”。你看,它甚至预判了学生用XAMPP装MySQL时默认密码为空、但Navicat连接却填了密码的尴尬场景。所以别把它当成一个“下载即用”的玩具,它是一套带着体温的、被反复验证过的教学基础设施。

2. 系统架构与分层设计:为什么坚持用Swing而不是JavaFX或Web?

2.1 选型逻辑:不是技术怀旧,而是教学精准度的必然选择

有人会问:都2025年了,为什么不用JavaFX?甚至用Spring Boot搭个简易Web后台加Thymeleaf前端,岂不更“现代”?这个问题我每次开课都会被学生问到,答案很实在:因为课程目标不是教框架,而是教“对象怎么协作”和“数据怎么落地”。 JavaFX的Scene Builder拖拽生成的FXML文件,对初学者来说就像黑盒——他能调出按钮,但说不清@FXML Button submitBtn;这行代码背后,Java虚拟机是怎么把XML节点映射成内存对象的;而Web开发一上来就要面对HTTP协议、Servlet生命周期、前后端分离的跨域问题,这些全在Java核心语法之外,会严重稀释“用面向对象思想解决实际问题”这个主线目标。

Swing恰恰卡在这个黄金平衡点上:它足够轻量,JFrame继承java.awt.FrameJButton继承javax.swing.AbstractButton,学生打开源码就能顺着继承链看到“组件本质就是Java对象”;它足够直观,panel.add(new JLabel("水果名称"))这种代码,和现实中往货架上摆水果的动作完全同构;它足够可控,没有异步回调地狱,所有事件都在EDT(Event Dispatch Thread)里顺序执行,调试时单步进去,每一步都能看到变量变化。这套水果摊系统的views包里,MainView.java只有127行,但它完整展示了MVC的雏形:JTable绑定DefaultTableModel(视图),ActionListener里调用FruitService.getFruitsByCategory()(控制器),而FruitService内部又调用FruitDao.findAll()(模型)。学生不需要懂MVC这个词,但他改一行tableModel.addRow(...)就能让界面上多出一条记录,这种即时反馈,是建立编程信心最有效的催化剂。

再看数据库层。为什么死磕MySQL 8.0而不是H2或Derby这类内存数据库?因为课程设计的终极考核点之一,是让学生亲手把“进货单”这种业务数据,变成硬盘上实实在在的.ibd文件。fruit.sql里那句CREATE TABLE fruit_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fruit_id BIGINT NOT NULL, quantity DECIMAL(10,2) DEFAULT 0.00, ... ),学生必须理解DECIMAL(10,2)意味着最多存99999999.99公斤水果,小数点后两位是为苹果按斤卖、橙子按件卖预留的精度——这种对业务规则的具象化约束,是内存数据库永远给不了的体感。而Navicat的引入,更是把抽象概念拉回地面:当学生在Navicat里右键点击fruit_stock表,选择“查看数据”,看到自己刚在程序里录入的“香蕉 25.50 kg”赫然在列时,那种“我的代码真的在操控真实世界”的震撼,比任何PPT都管用。

2.2 分层结构详解:src目录下的每一寸空间都有明确使命

打开压缩包里的src目录,你会看到标准的四层结构:entitydaoserviceviews,外加一个util。这不是为了炫技的分层,而是为了解耦“谁该关心什么”。我拿最核心的“进货登记”功能为例,拆解这四层如何像齿轮一样咬合:

  • entity/Fruit.java:这是业务世界的最小原子。它不关心数据库怎么存,也不管界面怎么显示,只定义“一个水果该有哪些属性”。比如price字段类型是BigDecimal而非double,因为0.1 + 0.2 != 0.3的浮点误差,在算进货成本时会导致分币级错误;unit字段用枚举UnitType.KGUnitType.ITEM限定取值,杜绝了数据库里出现unit='box'这种脏数据。

  • dao/FruitDao.java:这是数据世界的翻译官。它只做一件事:把Fruit对象和SQL语句互相转换。insert(Fruit fruit)方法里,预编译语句INSERT INTO fruit (name, unit, price) VALUES (?, ?, ?)的三个问号,严格对应fruit.getName()fruit.getUnit().getCode()fruit.getPrice().toString()。这里有个关键细节:getUnit().getCode()返回的是"kg"字符串,而不是直接存枚举名"KG",因为数据库字段是VARCHAR,存小写更符合通用规范,且避免大小写敏感带来的麻烦。

  • service/FruitService.java:这是业务逻辑的指挥中心。它协调DAO和Entity,处理跨表事务。比如“进货”操作,addPurchaseRecord(PurchaseRecord record)方法里,先调用fruitDao.updateStock(record.getFruitId(), record.getQuantity())更新库存,再调用purchaseDao.insert(record)保存进货单。如果第二步失败,整个方法会抛出RuntimeException,触发MySQL的自动回滚——这个机制,学生在fruit.sql脚本的SET AUTOCOMMIT = 0;注释里就能看到原理。

  • views/PurchaseView.java:这是用户世界的窗口。它只负责收集输入(JTextField获取数量)、校验格式(用NumberFormatter确保输入的是数字)、触发服务(点击按钮时调用fruitService.addPurchaseRecord(...)),然后刷新界面(stockTable.setModel(new StockTableModel(fruitService.getAllStocks())))。它绝不直接写SQL,也绝不计算总价——总价由FruitService.calculateTotalPrice()统一计算,保证业务规则只在一个地方定义。

util包则是润滑剂。DBConnectionUtil.java里封装了getConnection()方法,它读取db.properties配置文件(压缩包里有),自动拼接jdbc:mysql://localhost:3306/fruit_db?useSSL=false&serverTimezone=Asia/Shanghai这样的URL。为什么强调serverTimezone=Asia/Shanghai?因为MySQL 8.0默认时区是UTC,而学生电脑系统时间是东八区,不加这个参数,NOW()函数返回的时间会比本地时间慢8小时,导致“今日销售统计”永远少算半天数据。这种坑,文档里写了,代码里也防了,这才是真正的教学友好。

3. 数据库与Navicat实操:从建库到图表分析的全流程闭环

3.1 fruit.sql脚本深度解析:不只是建表,更是业务规则的代码化表达

fruit.sql这个文件,远不止是几条CREATE TABLE命令的集合。它是把水果摊老板的口头规矩,翻译成数据库能懂的语言。我们逐段拆解,看看那些看似枯燥的SQL背后,藏着多少教学设计的巧思:

-- 第一部分:创建数据库与用户权限
CREATE DATABASE IF NOT EXISTS fruit_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 创建专用用户,避免学生用root账号养成坏习惯
CREATE USER 'fruit_user'@'localhost' IDENTIFIED BY 'fruit123';
GRANT SELECT, INSERT, UPDATE, DELETE ON fruit_db.* TO 'fruit_user'@'localhost';
FLUSH PRIVILEGES;

这段代码的教学价值在于:它强制学生理解“数据库实例”和“数据库用户”是两个概念。很多学生第一次连不上Navicat,就是因为Navicat里填的是root密码,而fruit.sql创建的是fruit_user。文档里专门用红色字体提醒:“若连接失败,请检查Navicat连接设置中的用户名是否为’fruit_user’,密码为’fruit123’”。这比讲一百遍权限理论都管用。

-- 第二部分:水果主表,体现业务强约束
CREATE TABLE fruit (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL COMMENT '水果名称,如苹果、香蕉',
  unit ENUM('kg', 'item') NOT NULL COMMENT '计量单位,kg=公斤,item=件',
  price DECIMAL(10,2) NOT NULL COMMENT '单价,精确到分',
  created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

这里的ENUM('kg', 'item')是神来之笔。它比VARCHAR(10)加应用层校验更可靠——学生就算在Java代码里忘了校验unit,数据库也会拒绝插入unit='box'的数据。而created_timeupdated_time的自动赋值,让学生第一次直观看到“数据库也能记住时间”,为后续做“按时间范围查询销售记录”埋下伏笔。

-- 第三部分:库存表,实现“一物一码”的精准管理
CREATE TABLE fruit_stock (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  fruit_id BIGINT NOT NULL,
  quantity DECIMAL(10,2) DEFAULT 0.00,
  last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (fruit_id) REFERENCES fruit(id) ON DELETE CASCADE
);

FOREIGN KEY关联和ON DELETE CASCADE是重点。当学生在Navicat里删除一个已下架的水果(比如“榴莲”),库存表里所有fruit_id指向它的记录会自动消失。文档里用流程图展示这个过程:“删除水果 → 触发级联 → 库存清零 → 界面刷新显示0”,学生立刻明白“外键不是摆设,是业务安全的保险丝”。

-- 第四部分:初始化数据,让系统“活”起来
INSERT INTO fruit (name, unit, price) VALUES 
('苹果', 'kg', 8.50),
('香蕉', 'kg', 5.20),
('橙子', 'item', 3.00);
INSERT INTO fruit_stock (fruit_id, quantity) VALUES 
(1, 100.00), -- 苹果100公斤
(2, 85.50),  -- 香蕉85.5公斤
(3, 200);    -- 橙子200件

这12行INSERT,是整个系统启动的“氧气”。没有它,学生打开程序看到的是一片空白表格,信心瞬间崩塌。而数据设计本身就在教学:苹果香蕉按公斤,橙子按件,quantity字段的数值类型DECIMAL(10,2)完美匹配不同单位的精度需求。学生在Navicat里双击fruit_stock表,看到这三条初始数据,就知道“哦,原来库存是从这儿来的”。

3.2 Navicat实战指南:手把手教你用可视化工具验证代码逻辑

Navicat不是可选项,它是这套系统教学闭环的关键一环。很多课程设计只要求“程序能跑”,但学生根本不知道数据到底有没有进数据库。Navicat把这一切变得肉眼可见。以下是我在机房手把手教学生的标准流程:

第一步:建立连接(避坑重点)
在Navicat左上角“连接”→“MySQL”,弹出窗口填:
- 连接名:fruit_local(随便起,但建议统一)
- 主机名/IP地址:localhost(绝对不要填127.0.0.1!有些学生装了Docker,127.0.0.1会指向容器,而localhost才指向本机MySQL)
- 端口:3306(MySQL默认端口,除非你改过)
- 用户名:fruit_user(再次强调,不是root!)
- 密码:fruit123

提示:如果点击“测试连接”失败,90%的情况是MySQL服务没启动。Windows下按Win+R输入services.msc,找到MySQL80服务,右键“启动”。这个步骤在文档第3页有截图,但学生往往跳过,直接来问老师。

第二步:导入fruit.sql(一键式操作)
连接成功后,右键左侧连接名fruit_local→“运行SQL文件”,选择压缩包里的fruit.sql,勾选“执行所有查询”,点击“开始”。Navicat会自动创建数据库、用户、表,并插入初始数据。整个过程约15秒,完成后左侧“数据库”列表下会出现fruit_db,展开能看到fruitfruit_stock等5张表。

第三步:数据CRUD验证(教学核心环节)
这才是精华。让学生亲自操作,比讲十遍原理都有效:
- 查(Read):双击fruit表,右侧显示三条水果数据。让他们修改“苹果”的价格为9.00,按Ctrl+S保存,再刷新表格,确认变更生效。
- 增(Create):点击右上角“+”号,新增一行,填name='葡萄'unit='kg'price=15.80,保存。回到Java程序,点击“刷新水果列表”,新葡萄立刻出现在界面上——这就是DAO层findAll()的真实威力。
- 删(Delete):在fruit_stock表里,找到“橙子”那行(fruit_id=3),右键“删除所选行”,确认。然后回到Java程序,点“库存查询”,橙子库存显示为0。这时提问:“为什么库存没消失,而是变成0?” 引导学生看fruit_stock表结构里的DEFAULT 0.00,理解“删除库存记录”和“库存归零”是两回事。
- 改(Update):在fruit_stock表里,把“苹果”的quantity改成120.00,保存。再运行Java程序里的“进货登记”,输入苹果、数量10,提交。回到Navicat,刷新fruit_stock,看到数量变成130.00——证明updateStock()方法里的quantity = quantity + newQuantity逻辑正确执行。

第四步:简易图表分析(点燃兴趣的火种)
Navicat的图表功能,是让学生爱上数据的起点。右键purchase_record表(进货记录表)→“图表向导”,选择“柱状图”,X轴选date(进货日期),Y轴选total_amount(总金额),点击“完成”。一张按日期分布的进货金额图就出来了。学生会惊呼:“原来上周进了这么多香蕉!” 这种将代码逻辑转化为视觉洞察的瞬间,就是编程思维觉醒的时刻。文档第12页专门有这张图的截图,并标注:“此图表证明purchase_record表数据实时同步,可作为课程设计答辩的数据佐证”。

4. 核心功能模块实现:从UI布局到业务逻辑的完整链路

4.1 商品管理模块:Swing布局的艺术与事件驱动的严谨性

商品管理是系统的入口,也是学生最先接触的模块。views/FruitView.java这个类,表面看只是几个JTextFieldJButton的堆砌,但每一处布局都暗含教学意图。我们以添加新水果的流程为例,还原从点击按钮到数据落库的完整链路:

UI布局:GridBagLayout的克制使用
没有用BorderLayout这种简单粗暴的布局,也没有用GroupLayout这种复杂难懂的布局,而是选择了GridBagLayout——它像Excel表格一样,用行列坐标精确定位每个组件。FruitView.java里这段代码值得细品:

GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5); // 组件间留5像素空隙,视觉清爽
gbc.anchor = GridBagConstraints.WEST;  // 标签左对齐,符合中文阅读习惯

// 第一行:水果名称标签
gbc.gridx = 0; gbc.gridy = 0;
panel.add(new JLabel("水果名称:"), gbc);

// 第一行:名称输入框
gbc.gridx = 1;
JTextField nameField = new JTextField(15);
panel.add(nameField, gbc);

// 第二行:计量单位下拉框
gbc.gridy = 1;
gbc.gridx = 0;
panel.add(new JLabel("计量单位:"), gbc);
gbc.gridx = 1;
JComboBox<UnitType> unitCombo = new JComboBox<>(UnitType.values());
panel.add(unitCombo, gbc);

为什么这么麻烦?因为GridBagLayout强迫学生思考“组件的位置和大小由什么决定”。当学生想把“单价”输入框加宽,就必须修改JTextField(15)的参数,而不是盲目拖拽——这培养的是对代码控制UI的敬畏感。而Insets(5,5,5,5)这种细节,让界面在不同分辨率下都保持呼吸感,避免了学生交作业时截图糊成一团的尴尬。

事件驱动:ActionListener里的业务校验铁律
点击“添加水果”按钮触发的逻辑,是教学重点。FruitView.java里这段ActionListener,把“用户输入”到“数据库写入”的每一步都暴露在阳光下:

addBtn.addActionListener(e -> {
    // 步骤1:获取输入值(教学点:getText()返回String,需转换)
    String name = nameField.getText().trim();
    UnitType unit = (UnitType) unitCombo.getSelectedItem();
    BigDecimal price;
    try {
        price = new BigDecimal(priceField.getText().trim()); // 教学点:必须用BigDecimal,不用Double!
    } catch (NumberFormatException ex) {
        JOptionPane.showMessageDialog(this, "单价必须是数字!", "输入错误", JOptionPane.ERROR_MESSAGE);
        return; // 中断流程,不继续执行
    }

    // 步骤2:业务校验(教学点:校验规则必须前置)
    if (name.isEmpty()) {
        JOptionPane.showMessageDialog(this, "水果名称不能为空!", "输入错误", JOptionPane.ERROR_MESSAGE);
        return;
    }
    if (price.compareTo(BigDecimal.ZERO) <= 0) { // compareTo比==更安全
        JOptionPane.showMessageDialog(this, "单价必须大于0!", "输入错误", JOptionPane.ERROR_MESSAGE);
        return;
    }

    // 步骤3:调用服务层(教学点:分层的意义在此刻显现)
    try {
        Fruit fruit = new Fruit(name, unit, price);
        fruitService.addFruit(fruit); // 这行代码背后,是DAO层执行INSERT SQL
        JOptionPane.showMessageDialog(this, "添加成功!");
        refreshFruitTable(); // 刷新界面,显示新水果
    } catch (Exception ex) {
        JOptionPane.showMessageDialog(this, "添加失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
        ex.printStackTrace(); // 打印堆栈,方便学生定位问题
    }
});

这段代码的教学价值在于:它把“防御性编程”变成了肌肉记忆。学生看到try-catch包裹new BigDecimal(),就明白“用户输入不可信”;看到price.compareTo(BigDecimal.ZERO) <= 0,就记住“浮点数比较不能用==”;看到refreshFruitTable()addFruit()之后调用,就理解“界面更新必须依赖业务逻辑执行成功”。而最后一行ex.printStackTrace(),是留给学生的调试线索——当他们遇到SQLException: Duplicate entry时,看控制台输出,就能顺藤摸到fruit表的name字段没有加唯一索引这个根因(fruit.sql里确实没加,这是故意留的扩展题)。

4.2 库存与进销存核心:实时性、精度与事务安全的三角平衡

库存模块是整个系统的技术心脏,它必须同时满足三个苛刻条件:实时性(界面刷新要快)、精度(公斤和件不能算错)、事务安全(进货销售不能丢数据)。views/StockQueryPanel.javaservice/FruitService.java联手实现了这个平衡。

实时性保障:观察者模式的轻量实现
Swing原生不支持MVVM那样的双向绑定,但我们用最朴素的观察者模式解决。StockQueryPanel.java里有一个refreshStockTable()方法,它不是每次查询都重建JTable,而是复用DefaultTableModel

private void refreshStockTable() {
    List<StockInfo> stockList = fruitService.getAllStocks(); // 从服务层获取最新数据
    DefaultTableModel model = (DefaultTableModel) stockTable.getModel();
    model.setRowCount(0); // 清空旧数据,比新建TableModel更快
    for (StockInfo stock : stockList) {
        model.addRow(new Object[]{
            stock.getFruitName(),
            stock.getUnit(),
            stock.getQuantity(),
            stock.getLastUpdated()
        });
    }
}

关键在model.setRowCount(0)。学生如果用model = new DefaultTableModel(...)重建,会导致JTable重新渲染整个组件树,界面会闪一下;而setRowCount(0)只是清空数据行,表头和列宽保持不变,刷新如丝般顺滑。这个细节,文档里用性能对比截图展示:重建模型耗时12ms,清空行耗时0.8ms,差距15倍。

精度控制:BigDecimal与单位转换的硬核实践
库存精度是生死线。“苹果进货10.5公斤,卖出3.2公斤,剩余7.3公斤”这种计算,用double会得到7.300000000000001FruitService.java里所有涉及数量的计算,都用BigDecimal

public void updateStock(Long fruitId, BigDecimal quantityChange) {
    // 先查当前库存
    BigDecimal currentQty = stockDao.getQuantityByFruitId(fruitId);
    // 精确计算:current + change
    BigDecimal newQty = currentQty.add(quantityChange);
    // 更新库存(注意:add方法返回新对象,currentQty本身不变)
    stockDao.updateQuantity(fruitId, newQty);
}

更绝的是单位转换。StockInfo.java里有个getQuantityDisplay()方法:

public String getQuantityDisplay() {
    if (unit.equals(UnitType.KG)) {
        return quantity.setScale(2, RoundingMode.HALF_UP) + " kg"; // 保留2位小数
    } else {
        return quantity.setScale(0, RoundingMode.HALF_UP) + " 件"; // 件数取整
    }
}

setScale(2, RoundingMode.HALF_UP)确保10.555公斤显示为10.56 kgsetScale(0, RoundingMode.HALF_UP)确保200.7件橙子显示为201 件。这种对业务场景的极致适配,是教科书里找不到的实战智慧。

事务安全:MySQL的ACID在代码中的具象化
进销存最怕“只记了进货,没扣库存”或反之。FruitService.javaprocessSale()方法用@Transactional注解(基于Spring的简化版,实际用Connection.setAutoCommit(false)手动实现):

public void processSale(SaleRecord record) {
    Connection conn = null;
    try {
        conn = DBConnectionUtil.getConnection();
        conn.setAutoCommit(false); // 关闭自动提交,开启事务

        // 步骤1:扣减库存(可能抛出异常:库存不足)
        BigDecimal currentStock = stockDao.getQuantityByFruitId(record.getFruitId());
        if (currentStock.compareTo(record.getQuantity()) < 0) {
            throw new RuntimeException("库存不足!当前:" + currentStock + ",需:" + record.getQuantity());
        }
        stockDao.updateQuantity(record.getFruitId(), currentStock.subtract(record.getQuantity()));

        // 步骤2:保存销售记录
        saleDao.insert(record);

        conn.commit(); // 两步都成功,才提交
    } catch (Exception e) {
        if (conn != null) {
            try {
                conn.rollback(); // 任一步失败,全部回滚
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
        throw e;
    }
}

这段代码,就是ACID(原子性、一致性、隔离性、持久性)的Java翻译。学生在Navicat里故意把苹果库存设为5.00,然后在程序里尝试卖10.00公斤,会看到弹窗“库存不足”,而Navicat里库存仍是5.00——事务的原子性,此刻无比真实。

5. 开发文档与二次开发指南:让代码成为可生长的生命体

5.1 开发文档(.docx)的隐藏价值:不是说明书,而是成长地图

那份名为开发文档 java大作业-水果摊信息管理系统.docx的Word文件,我敢说90%的学生只会扫一眼封面就扔一边。但它真正的价值,在于把“如何成为一个合格的开发者”这个宏大命题,拆解成可执行的步骤。文档结构本身就是一门课:

  • 第1章 需求说明:不是罗列功能,而是用场景故事呈现。比如“进货登记”需求,写的是:“王老板每天清晨去批发市场进货,需要快速录入水果名称、重量、单价,系统自动计算总金额,并更新库存。他希望历史进货单能按日期查询,以便月底对账。”——这让学生明白,代码不是写给机器看的,是写给活生生的人用的。

  • 第2章 系统架构图:用PlantUML生成的简洁图示,只画了ViewsServiceDAODatabase四个方块,箭头标注“调用”和“存储”。没有画UML序列图那种复杂玩意,因为学生还没到那个阶段。图下方有一行小字:“此图说明:界面不直接操作数据库,所有数据流动必须经过Service层——这是分层架构的第一铁律。”

  • 第3章 模块功能详述:每个功能点都配“预期效果”和“常见问题”。比如“库存查询”模块,预期效果是“启动程序后,默认显示所有水果当前库存”,而常见问题是:“为什么首次启动时库存表为空?答:请先执行fruit.sql脚本导入初始数据,详见第5章。”——这教会学生,问题从来不是孤立的,它一定在某个上下文里。

  • 第4章 运行环境配置:精确到路径。JDK 8写的是“推荐使用jdk1.8.0_291(官网下载链接)”,MySQL 8.0写的是“安装时务必勾选‘Add MySQL to PATH’”,Navicat写的是“版本需≥15.0,低版本不支持MySQL 8.0的caching_sha2_password认证插件”。这些细节,省去了学生在网上搜三天还配不好的绝望。

  • 第5章 关键界面截图:每张截图都带红色箭头和文字标注。比如主界面截图,箭头指着菜单栏的“进货管理”,文字写着:“点击此处进入进货登记界面,所有功能入口均在此菜单下,符合桌面应用交互规范。”——这其实在潜移默化地教UI设计原则。

文档最后一页是“致同学”,写着:“恭喜你读到这里!这意味着你已经具备了读懂一个真实项目的能力。下一步,试着做一件小事:把‘销售记录’表格的‘销售日期’列,从显示2025-04-12改为2025年04月12日。答案不在文档里,但在util/DateUtils.java中。动手吧,程序员的世界,始于第一个成功的修改。”——这就是成长地图的终点,也是起点。

5.2 二次开发实战:从“改一个按钮文字”到“加一个新报表”

这套系统的设计哲学是:“所有扩展,都不应破坏现有结构。” 我以两个典型二次开发任务为例,展示如何像老司机一样安全地改装这辆“水果摊汽车”。

任务1:修改按钮文字(入门级,建立信心)
需求:把“进货登记”按钮的文字,改成“新增进货单”。
步骤:
1. 在src/views/目录下,找到PurchaseView.java(进货界面类)。
2. 搜索new JButton("进货登记"),定位到创建按钮的代码行。
3. 把"进货登记"改成"新增进货单"
4. 保存,重新编译(或直接运行Fruit.java),按钮文字立刻改变。

实操心得:这是最安全的修改,因为它只影响UI层,不涉及业务逻辑。学生做完会获得“我能改代码”的原始自信。文档里把这个任务列为“Level 1”,并提示:“修改后,检查按钮宽度是否足够显示新文字,若文字被截断,可调整JButtonsetPreferredSize(new Dimension(120, 25))”。

任务2:增加销售日报表(进阶级,理解数据流)
需求:在菜单栏增加“报表”→“销售日报”,点击后弹出窗口,显示今天所有销售记录的汇总(总笔数、总金额、最畅销水果)。
步骤:
1. 新增UI类:在src/views/下新建SalesReportView.java,继承JDialog,用GridBagLayout布局,放三个JLabel显示汇总数据。
2. 新增Service方法:在src/service/FruitService.java里,添加方法:
java public SalesReportData getTodaySalesReport() { LocalDate today = LocalDate.now(); List<SaleRecord> todaySales = saleDao.findByDate(today); BigDecimal totalAmount = todaySales.stream() .map(SaleRecord::getTotalAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); // 找最畅销水果(按销量count) Map<Long, Long> fruitCountMap = todaySales.stream() .collect(Collectors.groupingBy(SaleRecord::getFruitId, Collectors.counting())); Long topFruitId = fruitCountMap.entrySet().stream() .max(Map.Entry.comparingByValue()) .map(Map.Entry::getKey) .orElse(-1L); String topFruitName = (topFruitId == -1L) ? "无" : fruitDao.findById(topFruitId).getName(); return new SalesReportData(todaySales.size(), totalAmount, topFruitName); }
3. 在主菜单添加入口:打开src/views/MainView.java,找到菜单栏创建代码,在“帮助”菜单前插入:
java JMenu reportMenu = new JMenu("报表"); JMenuItem dailyReportItem = new JMenuItem("销售日报"); dailyReportItem.addActionListener(e -> { SalesReportData data = fruitService.getTodaySalesReport(); new SalesReportView(this, data).setVisible(true); // 弹出报表窗口 }); reportMenu.add(dailyReportItem); menuBar.add(reportMenu);
4. 测试:运行程序,点击“报表”→“销售日报”,窗口弹出,显示今日数据。

实操心得:这个任务覆盖了UI、Service、DAO三层,但每一处修改都遵循原有风格。getTodaySalesReport()方法里用LocalDate.now()而不是new Date(),因为前者是Java 8时间API,更安全;Collectors.groupingBy用法,是Stream API的经典实践。学生做完,就真正理解了“一个新功能,是如何从界面请求,穿过服务层,最终从数据库捞出数据”的全链路。文档里把这个列为“Level 3”,并警告:“若报表数据不准,请检查saleDao.findByDate()方法中SQL的日期格式是否为'2025-04-12',MySQL的DATE类型不认'2025/04/12'。”

6. 常见问题排查与避坑指南:那些让你抓狂半小时的“小问题”

6.1 环境配置类问题:90%的“运行失败”都源于此

学生在机房最常喊的那句“老师,我点Fruit.bat没反应!”,背后往往藏着几个经典陷阱。我把它们整理成速查表,按发生频率排序:

问题现象 根本原因 快速诊断法 一招解决
双击Fruit.bat窗口一闪而逝 JAVA_HOME未设置或指向JRE而非JDK 右键编辑Fruit.bat,看第一行set JAVA_HOME=...路径是否包含jdk字样 下载JDK 8,安装后在系统环境变量中设置JAVA_HOME为JDK安装目录,如C:\Program Files\Java\jdk1.8.0_291
Navicat连接MySQL失败,提示“Access denied” 用户名密码错误,或MySQL服务未启动 在命令行输入mysql -u root -p,若同样失败,则MySQL服务未运行 Win+Rservices.msc→找到MySQL80→右键“启动”;若密码忘记,用管理员身份运行CMD,执行mysqld --skip-grant-tables重置
程序启动后界面空白,控制台报ClassNotFoundException: com.mysql.cj.jdbc.Driver MySQL JDBC驱动jar包未正确加载 查看Fruit.batjava -cp参数,确认路径包含mysql-connector-java-8.0.28.jar 将压缩包里的mysql-connector-java-8.0.28.jar复制到lib目录(若无则新建),并在Fruit.bat-cp参数中加入lib\mysql-connector-java-8.0.28.jar
中文乱码:界面上显示“???”或数据库里存的是“????” MySQL连接URL缺少字符集参数 查看DBConnectionUtil.javagetConnection()方法,URL是否包含characterEncoding=utf8mb4 修改URL为jdbc:mysql://localhost:3306/fruit_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4

注意:以上所有问题,在开发文档.docx的“附录A 常见问题速查”里都有对应截图和解决方案。但学生往往不看文档,直接问老师。所以我在机房贴了一张A4纸,标题就叫“Fruit系统启动三板斧”,内容就是上面表格的前三行,用红笔圈出“JAVA_HOME”、“services.msc”、“lib目录”,学生一抬头就能看见。

6.2 业务逻辑类问题:那些藏在代码深处的“幽灵Bug”

有些问题不会让程序崩溃,但会让业务结果离谱。它们像幽灵一样,需要学生真正读懂代码才能揪出。

Bug 1:“库存越卖越多”
现象:明明卖了10公斤苹果,库存反而从100变成110。
诊断:打开FruitService.java,找到processSale()方法,看库存更新那行:

// 错误写法(学生常犯)
stockDao.updateQuantity(fruitId, currentStock.add(record.getQuantity())); // 加号!
// 正确写法
stockDao.updateQuantity(fruitId, currentStock.subtract(record.getQuantity())); // 减号!

根源:学生把“销售”理解成“进货”,符号写反了。文档里用加粗红字警告:“processSale()方法中,库存更新必须用subtract(),这是业务规则的硬性要求”。

Bug 2:“日期查询总是空”
现象:在销售查询界面选“今天”,结果列表为空,但Navicat里明明有今天的数据。
诊断:检查saleDao.findByDate(LocalDate date)方法里的SQL:

-- 错误SQL(用=号精确匹配)
SELECT * FROM sale_record WHERE date = '2025-04-12';
-- 正确SQL(用BETWEEN匹配全天)
SELECT * FROM sale_record WHERE date BETWEEN '2025-04-12' AND '2025-04-12';

根源:MySQL的DATE类型存储的是2025-04-12,但Java的LocalDate.now()生成的也是2025-04-12BETWEEN更安全。这个细节,在dao/SaleDao.java的Javadoc里有明确注释:“此方法使用BETWEEN确保覆盖当日00:00:00至23:59:59的所有记录”。

Bug 3:“单价显示为0.00”
现象:在水果列表里,所有水果单价都显示0.00,但Navicat里数据是正确的。
诊断:打开entity/Fruit.java,看price字段的getter:

// 错误写法(返回了错误的字段)
public BigDecimal getPrice() {
    return this.unit; // 把unit枚举当price返回了!
}
// 正确写法
public BigDecimal getPrice() {
    return this.price;
}

根源:IDE自动生成getter时选错了字段。这种低级错误,恰恰说明学生没有养成“写完代码立刻用Navicat验证数据流向”的习惯。所以文档里强制要求:“每次修改Entity类后,必须重启程序,检查对应界面数据是否正确显示”。

6.3 性能与体验优化技巧:让系统从“能用”到“好用”

一套课程设计系统,不仅要跑通,还要让学生愿意多用几分钟。这些优化技巧,都是我在批改上百份作业后总结的:

  • 启动速度优化Fruit.java主类里,main方法第一行是JFrame.setDefaultLookAndFeelDecorated(true);,这行代码让Swing界面使用系统原生外观(Windows就是Windows风格),比Java默认的Metal风格启动快300ms。学生感觉不到,但老师验收时批量启动20台电脑,能省下整整10分钟。

  • 输入体验优化:所有JTextField都设置了setFont(new Font("微软雅黑", Font.PLAIN, 14));,字号14比默认12更易读;所有数字输入框(如价格、数量)都绑定了NumberFormatter,用户输入abc时,框里自动变为空白,而不是报错弹窗——这降低了初学者的挫败感。

  • 错误反馈优化JOptionPane.showMessageDialog()的图标,严格区分:ERROR_MESSAGE用于输入校验失败(如价格非数字),INFORMATION_MESSAGE用于操作成功(如添加成功),WARNING_MESSAGE用于业务警告(如库存低于10公斤)。颜色和图标的一致性,本身就是一种用户体验教育。

最后分享一个小技巧:如果学生想快速验证自己的修改是否影响核心功能,不必从头跑全流程。只需打开src/test/SmokeTest.java(压缩包里有),这是一个极简的冒烟测试类,它会:
1. 调用fruitService.addFruit(new Fruit("测试水果", UnitType.KG, new BigDecimal("1.00")))
2. 调用fruitService.getFruitByName("测试水果")确认存在
3. 调用fruitService.deleteFruitById(...)清理
4. 打印“Smoke test passed!”到控制台

运行这个类,1秒内就能确认DAO层增删查改全部正常。这比手动点界面快十倍,是高效迭代的基石。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:面向高校Java课程设计的桌面端水果摊进销存系统,基于Swing开发图形界面,支持商品管理、库存实时查询、进货/销售记录登记、员工信息维护等实用功能。后端使用MySQL 8.0数据库,压缩包内附fruit.sql建表及初始化数据脚本,可直接在Navicat中导入并执行可视化操作,包括连接配置、表结构浏览、CRUD操作和简易图表分析,方便教学演示与作业验收。源码结构清晰,严格遵循分层设计(entity/dao/views/util),含全部.java源文件、编译后的.class文件、MySQL JDBC驱动jar包、UML类图(diagram.uml)、UI配置文件及详细开发文档(Word格式),涵盖需求说明、系统架构、模块划分、运行环境配置(JDK 8 + Windows验证通过)、关键界面截图与启动方式(主类Fruit)。所有代码无加密、无混淆,注释完整,适合作为课程大作业参考、自学练手项目或二次开发基础模板。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐