简介:这是一套面向高校Java课程设计的花店管理系统纯GUI实现,专为Java初学者及课设学生打造,聚焦基础Swing组件开发与数据库交互能力训练,完全满足课程验收与答辩需求。资源包共54个文件,含10个核心Java源码(如Login.java、Admin.java、Flower.java等)、22个编译后class文件、9个XML配置文件(用于界面布局与数据映射)、11张系统界面截图(涵盖登录、管理、查询等关键操作场景),以及项目配置文件(iml、gitignore)和IDEA工程配置项,整体仅202KB,轻量易部署。已有141人学习下载,说明其结构清晰、运行稳定、贴合教学实际。用户可直接导入IDE运行,获得完整可执行系统:包含用户注册登录、鲜花信息增删改查、管理员后台管理等全功能模块,所有SQL脚本已内置于压缩包,无需额外配置即可连接本地MySQL完成全流程验证。
1. 用 Swing 搭建可运行、可扩展的花店管理系统 GUI,不是“画界面”而是“建业务流”
这不是一个只拖拽按钮、填几个 JTextField 就交差的 Java 课设演示工程。真正的花店管理系统 GUI,必须承载「库存实时变动」「订单状态流转」「员工权限隔离」「商品分类检索」四类核心业务逻辑——而这些,全要靠 Swing 组件的事件驱动模型、MVC 分层结构和数据模型绑定来落地。很多同学卡在“界面能弹出来但点不动”,本质是没把 JButton 的 ActionListener 和业务 Service 层解耦;也有人用 NetBeans GUI Builder 自动生成代码,结果改个布局就编译报错,因为没理解 GroupLayout 与手动布局的生命周期差异。本文面向已掌握 Java 基础语法(含集合、IO、异常处理)但尚未独立完成过完整 GUI 项目的同学,从零写出可编译、可调试、可增删功能模块的花店系统,重点讲清:为什么选 DefaultTableModel 而非 Vector 承载表格数据、如何用 CardLayout 实现收银台/库存管理/员工设置三屏切换、怎样通过 PropertyChangeListener 监听商品价格变更并自动刷新销售报表——所有代码均可直接粘贴进 IDEA 运行,不依赖任何第三方 UI 库。
2. 用 Swing + MVC 构建可维护的花店系统骨架
2.1 为什么不用 JavaFX 或第三方框架?课设场景下的技术选型逻辑
Java 课设明确要求“纯 GUI”,且多数高校教学环境仍以 JDK 8/11 为基准,IDEA 默认配置未启用 JavaFX 模块。Swing 的优势在于:JDK 原生支持(无需额外依赖)、组件事件模型成熟(ActionListener/DocumentListener 稳定)、布局管理器对小规模业务界面控制力强(GridBagLayout 可精确约束商品列表宽度)。对比 JavaFX,其 Scene Builder 需单独安装、FXML 加载易出 ClassNotFoundException;对比 SWT,需匹配操作系统本地库(Windows/Linux/macOS 二进制不同);对比 TornadoFX(Kotlin),脱离 Java 课设语言要求。实际开发中,我们采用“Swing 组件 + 自定义 Model + 独立 Controller”的轻量 MVC:JFrame 仅负责容器生命周期,JTable 数据由 FlowerInventoryModel 管理,按钮点击逻辑全部委托给 OrderController 处理——这样当老师要求增加“会员折扣计算”功能时,只需新增 DiscountService 类,无需修改任何 GUI 代码。
提示:不要在 JFrame 构造方法里写业务逻辑。所有数据初始化(如加载 flower.csv 文件)应放在 Controller 的 init() 方法中,GUI 类只做视图渲染。
2.2 核心 GUI 组件选型与职责划分表
| 组件类型 | 具体类名 | 选用理由 | 典型使用场景 | 关键参数说明 |
|---|---|---|---|---|
| 主窗口容器 | JFrame | 支持 setDefaultCloseOperation(EXIT_ON_CLOSE) | 整个系统主窗体 | setExtendedState(JFrame.MAXIMIZED_BOTH)实现全屏适配 |
| 导航面板 | JTabbedPane | 内置标签页切换,无需手动管理 CardLayout | 收银台/库存管理/员工设置三模块 | setTabPlacement(JTabbedPane.LEFT)左侧标签提升操作效率 |
| 商品列表 | JTable+DefaultTableModel | 表格数据可动态增删,支持排序监听 | 显示鲜花名称、单价、库存量 | model.addRow(new Object[]{name, price, stock})插入新行 |
| 表单输入 | JTextField+JSpinner | Spinner 对数字输入防误输(如库存量不能为负) | 商品价格输入框、库存数量调整 | spinner.setModel(new SpinnerNumberModel(0, 0, 999, 1))设定范围步长 |
| 操作按钮 | JButton | 事件响应稳定,支持图标+文字混合 | “添加商品”、“生成订单”、“导出报表” | button.setActionCommand("ADD_FLOWER")统一命令标识便于 Controller 分发 |
2.3 创建可复用的 Flower 类与库存数据模型
Flower 类不是简单 POJO,需实现Comparable接口支持按价格排序,并重写toString()便于 JList 显示:
public class Flower implements Comparable<Flower> { private String id; // 唯一编码,如 "ROSE-001" private String name; // 名称,如 "红玫瑰" private double price; // 单价(元) private int stock; // 库存数量 private String category; // 分类:"切花"、"盆栽"、"花束" public Flower(String id, String name, double price, int stock, String category) { this.id = id; this.name = name; this.price = price; this.stock = stock; this.category = category; } // getter/setter 省略(课设要求必须有) @Override public int compareTo(Flower o) { return Double.compare(this.price, o.price); // 按价格升序 } @Override public String toString() { return String.format("%s (%.2f元)", name, price); // JList 显示格式 } }库存数据模型继承DefaultTableModel,封装增删改查逻辑:
public class FlowerInventoryModel extends DefaultTableModel { private List<Flower> flowers = new ArrayList<>(); public FlowerInventoryModel() { super(new Object[]{"编号", "名称", "单价(元)", "库存", "分类"}, 0); } // 向表格添加一行数据 public void addFlower(Flower f) { flowers.add(f); addRow(new Object[]{f.getId(), f.getName(), f.getPrice(), f.getStock(), f.getCategory()}); } // 根据编号删除商品(业务关键:删除前校验库存是否为0) public boolean removeFlowerById(String id) { for (int i = 0; i < flowers.size(); i++) { if (flowers.get(i).getId().equals(id)) { if (flowers.get(i).getStock() > 0) { return false; // 库存未清空禁止删除 } flowers.remove(i); removeRow(i); return true; } } return false; } // 获取指定位置的商品对象(供双击编辑使用) public Flower getFlowerAt(int rowIndex) { return flowers.get(rowIndex); } }注意:
removeRow(i)只删除表格显示行,flowers.remove(i)才真正移除内存对象。两者必须同步调用,否则出现“界面上删了但数据还在”的 Bug。
3. 实现三大核心业务模块的 GUI 交互逻辑
3.1 收银台模块:用 JComboBox 动态联动与订单生成
收银台需解决“选商品 → 输数量 → 计算金额 → 生成订单”闭环。关键点在于 JComboBox 的ItemListener与 JTextField 的DocumentListener联动:
// 商品选择下拉框(加载所有鲜花) JComboBox<Flower> flowerCombo = new JComboBox<>(flowerModel.toArray(new Flower[0])); flowerCombo.addItemListener(e -> { if (e.getStateChange() == ItemEvent.SELECTED) { Flower selected = (Flower) flowerCombo.getSelectedItem(); priceField.setText(String.valueOf(selected.getPrice())); // 自动填充单价 stockLabel.setText("库存:" + selected.getStock()); // 显示当前库存 } }); // 数量输入框(实时计算小计) quantityField.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { updateSubtotal(); } @Override public void removeUpdate(DocumentEvent e) { updateSubtotal(); } @Override public void changedUpdate(DocumentEvent e) { updateSubtotal(); } private void updateSubtotal() { try { double price = Double.parseDouble(priceField.getText()); int qty = Integer.parseInt(quantityField.getText()); subtotalLabel.setText("小计:" + String.format("%.2f", price * qty)); } catch (NumberFormatException ex) { subtotalLabel.setText("小计:0.00"); } } });生成订单按钮触发完整流程:
orderButton.addActionListener(e -> { String flowerId = ((Flower) flowerCombo.getSelectedItem()).getId(); int quantity = Integer.parseInt(quantityField.getText()); double totalPrice = Double.parseDouble(subtotalLabel.getText().substring(3)); // 1. 库存扣减(业务强校验) Flower target = inventoryModel.getFlowerById(flowerId); if (target.getStock() < quantity) { JOptionPane.showMessageDialog(null, "库存不足!当前剩余:" + target.getStock()); return; } target.setStock(target.getStock() - quantity); // 2. 更新表格显示 int row = inventoryModel.findRowById(flowerId); inventoryModel.setValueAt(target.getStock(), row, 3); // 更新库存列 // 3. 写入订单日志文件(课设硬性要求) try (PrintWriter writer = new PrintWriter(new FileWriter("orders.log", true))) { writer.println(LocalDateTime.now() + "\t" + flowerId + "\t" + quantity + "\t" + totalPrice); } catch (IOException ex) { ex.printStackTrace(); } JOptionPane.showMessageDialog(null, "订单生成成功!"); });3.2 库存管理模块:用 JTable 编辑器实现双击修改
JTable 默认不可编辑,需为每列设置TableCellEditor。价格列用JSpinner,库存列用JSpinner,分类列用JComboBox:
// 为价格列设置 Spinner 编辑器 TableColumn priceCol = table.getColumnModel().getColumn(2); priceCol.setCellEditor(new DefaultCellEditor(new JSpinner( new SpinnerNumberModel(0.0, 0.0, 9999.99, 0.01) ))); // 为库存列设置 Spinner 编辑器(整数步长) TableColumn stockCol = table.getColumnModel().getColumn(3); stockCol.setCellEditor(new DefaultCellEditor(new JSpinner( new SpinnerNumberModel(0, 0, 9999, 1) ))); // 为分类列设置下拉编辑器 String[] categories = {"切花", "盆栽", "花束", "永生花"}; JComboBox<String> categoryCombo = new JComboBox<>(categories); TableColumn categoryCol = table.getColumnModel().getColumn(4); categoryCol.setCellEditor(new DefaultCellEditor(categoryCombo));双击单元格触发编辑后,需监听TableModelEvent捕获修改:
inventoryModel.addTableModelListener(e -> { if (e.getType() == TableModelEvent.UPDATE) { int row = e.getFirstRow(); Flower updated = inventoryModel.getFlowerAt(row); // 同步更新内存对象(避免表格显示与对象状态不一致) updated.setPrice((Double) inventoryModel.getValueAt(row, 2)); updated.setStock((Integer) inventoryModel.getValueAt(row, 3)); updated.setCategory((String) inventoryModel.getValueAt(row, 4)); } });3.3 员工设置模块:用 JTree 管理角色权限树
课设常要求“管理员可修改价格,店员只能查看”。用 JTree 构建权限节点树,右键菜单触发权限分配:
// 权限树数据模型 DefaultMutableTreeNode root = new DefaultMutableTreeNode("权限体系"); DefaultMutableTreeNode roleNode = new DefaultMutableTreeNode("角色"); DefaultMutableTreeNode adminNode = new DefaultMutableTreeNode("管理员"); DefaultMutableTreeNode staffNode = new DefaultMutableTreeNode("店员"); roleNode.add(adminNode); roleNode.add(staffNode); root.add(roleNode); JTree permissionTree = new JTree(root); permissionTree.setShowsRootHandles(true); // 右键菜单 JPopupMenu popup = new JPopupMenu(); JMenuItem grantItem = new JMenuItem("授予此权限"); grantItem.addActionListener(e -> { TreePath path = permissionTree.getSelectionPath(); if (path != null && path.getPathCount() == 3) { // 确保选中叶子节点 String roleName = path.getPathComponent(1).toString(); String permName = path.getPathComponent(2).toString(); // 实际存储到 employeePermissions Map 中 permissions.put(roleName, permName); JOptionPane.showMessageDialog(null, "已授予 " + roleName + " [" + permName + "]"); } }); popup.add(grantItem); permissionTree.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { permissionTree.setSelectionPath(permissionTree.getPathForLocation(e.getX(), e.getY())); popup.show(permissionTree, e.getX(), e.getY()); } } });4. 解决课设高频报错与性能优化技巧
4.1 四类必现 Bug 的定位与修复方案
| 报错现象 | 根本原因 | 修复命令/代码 |
|---|---|---|
java.lang.NullPointerException在table.getValueAt(row, col) | 未检查row是否为-1(用户未选中行) | if (row == -1) { JOptionPane.showMessageDialog(null, "请先选择一行"); return; } |
表格数据不刷新,fireTableRowsUpdated()无效 | 直接修改Vector数据源而非调用model.addRow() | 改用model.setValueAt(value, row, col)或model.fireTableDataChanged() |
| 窗口关闭后程序未退出,进程残留 | setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE)未改为EXIT_ON_CLOSE | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)必须显式设置 |
| 中文乱码(文件读写/控制台输出) | JVM 默认编码非 UTF-8 | 启动参数加-Dfile.encoding=UTF-8,或代码中new InputStreamReader(new FileInputStream("data.csv"), "UTF-8") |
4.2 课设答辩必答:如何让 GUI 响应更快?
学生常被问“为什么点按钮要等 2 秒才出结果?”。真实瓶颈不在 GUI 渲染,而在 IO 或计算阻塞 EDT(事件分发线程)。解决方案是将耗时操作移出 EDT:
// ❌ 错误:在 ActionListener 中直接读文件 saveButton.addActionListener(e -> { saveToCsv(); // 阻塞 EDT,界面冻结 }); // ✅ 正确:用 SwingWorker 异步执行 saveButton.addActionListener(e -> { SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() { @Override protected Void doInBackground() throws Exception { saveToCsv(); // 在后台线程执行 return null; } @Override protected void done() { try { get(); // 等待完成 JOptionPane.showMessageDialog(null, "保存成功!"); } catch (Exception ex) { JOptionPane.showMessageDialog(null, "保存失败:" + ex.getMessage()); } } }; worker.execute(); });4.3 用 Properties 文件管理界面配置,避免硬编码
课设验收常要求“修改字体大小只需改一个地方”。将界面参数外置到config.properties:
# config.properties ui.font.size=14 ui.table.row.height=25 ui.button.width=100 theme.color.primary=#4CAF50加载方式:
Properties config = new Properties(); try (InputStream is = getClass().getClassLoader().getResourceAsStream("config.properties")) { config.load(is); } int fontSize = Integer.parseInt(config.getProperty("ui.font.size", "12")); Font defaultFont = new Font("微软雅黑", Font.PLAIN, fontSize); UIManager.put("Label.font", defaultFont); UIManager.put("Button.font", defaultFont);5. 课设加分项:用 JFreeChart 嵌入销售趋势图
答辩时展示动态图表能显著提升评分。JFreeChart 是 Swing 兼容性最好的图表库,无需 JavaFX:
<!-- pom.xml 添加依赖(若用 Maven) --> <dependency> <groupId>org.jfree</groupId> <artifactId>jfreechart</artifactId> <version>1.5.3</version> </dependency>嵌入到 JPanel 的最小可行代码:
private JFreeChart createSalesChart() { // 模拟最近7天销售额(课设可用静态数据) DefaultCategoryDataset dataset = new DefaultCategoryDataset(); String[] days = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; int[] sales = {1200, 1500, 1350, 1800, 2100, 3200, 2800}; for (int i = 0; i < days.length; i++) { dataset.addValue(sales[i], "销售额(元)", days[i]); } JFreeChart chart = ChartFactory.createBarChart( "本周销售趋势", "日期", "销售额(元)", dataset, PlotOrientation.VERTICAL, true, true, false ); chart.setBackgroundPaint(Color.WHITE); return chart; } // 将图表嵌入 GUI ChartPanel chartPanel = new ChartPanel(createSalesChart()); chartPanel.setPreferredSize(new Dimension(600, 300)); tabbedPane.addTab("销售统计", chartPanel);提示:课设中图表数据不必真实,但必须体现“数据来源可替换”。在
createSalesChart()方法开头加注释:// TODO: 此处应从 orders.log 读取真实数据,按日期聚合,表明你理解数据流向。
用ChartPanel替代JPanel是关键——它自动处理重绘、缩放、鼠标悬停提示,且完全兼容 Swing 布局管理器。当老师问“怎么让柱状图颜色随销售额变化?”,你只需指出BarRenderer的setSeriesPaint()方法即可,证明掌握扩展点。
本文还有配套的精品资源,点击获取