
简介一套面向Java课程设计/期末大作业的完整管理系统实现水果摊信息管理技术路线为Java配合MySQL数据库并借助Navicat完成数据可视化界面。项目源自211大学课程设计作者期末93分、专业排名第二功能完整度与文档规范度均可作为同类管理系统的参考模板。压缩包共61个文件约9.74MB既有12个Java源码和23个编译后的class文件也有SQL数据库脚本、XML配置、UML类图、Word开发文档以及运行演示gif覆盖从数据库导入、工程运行到界面演示与结构解读的完整链路。已有1755人学习/下载说明其在期末大作业场景下具有一定认可度。学习者可重点阅读开发文档理清业务模块结合entity、dao、util、views等分层代码掌握JDBC操作与界面交互思路SQL脚本可一键还原数据库自带的mysql-connector-java-8.0.17.jar避免驱动配置门槛gif演示用于比对运行效果整体是一份可复用、可二次开发、也可参考撰写报告的优质Java课程设计资源。1. 水果摊管理系统把期末大作业做成一张关系模型地图水果摊听起来不像正经软件但它背后就是一套完整的进销存模型有供应商、有进货、有库存、有销售还有毛利核算。这个规模的业务刚好卡在「单表 CRUD 明显不够企业 ERP 又过于复杂」的中间是 Java Web 期末大作业里最适合展示数据库功底的场景。评分老师看的三件事基本不会变表设计合理不合理、CRUD 有没有写全、数据能不能在 MySQL 和 Navicat 之间对得上。这套管理系统我建议你把它当成一次「数据库建模 JDBC 封装 可视化管理」的联合练习来做前端只是皮相。核心是让 MySQL 把数据关系理清楚再用 Navicat 把进货、库存、售卖的流转过程变成一眼能看懂的东西。适合哪些人读Java 基础语法已经过完、准备交课设的学生也适合刚入职需要快速搭建内部管理系统原型的开发。下文直接按我通常落地的顺序来先建模再连库然后做业务流程最后用 Navicat 检查每一环。2. 用 MySQL 设计水果摊的表结构数据库定生死2.1 先把需求拆成四类业务对象水果摊信息管理系统的功能清单每个同学写的都不一样但归纳起来跑不出四个业务对象水果本身的档案信息、供应商渠道、采购进货记录、销售流水。再加上一个登录用的用户表一共五张表就够覆盖大作业的评分页面。少了显得单薄多了一堆用不上的表反而暴露设计短板。我一般会把需求落到一张关系清单上一个供应商能往店里送多种水果所以「水果表」和「供应商表」是多对一每次进货产生一条进货单进货明细里必须记录当次进价和数量每次销售记录谁买了多少、按什么价格卖。这里最关键的一条经验是别把「进货价」直接挂在水果表上长期使用因为水果的价格是波动的今天两块钱进的西瓜和明天两块五进的西瓜不应该互相覆盖。价格属于单据不属于档案。2.2 五张表的字段设计与数据类型对照这里直接给出我推荐的建表结构。fruit 表只放水果当前档案价格字段只做展示用的参考值真正的交易价格全部落在 purchase 和 sale 两张明细表里。表名用途核心字段关键约束fruit水果档案与库存id, name, category, unit, purchase_price, sale_price, stock, safety_stockstock 为当前库存快照safety_stock 为最低预警线supplier供应商档案id, name, contact, phone, address一个供应商对多个水果purchase进货流水id, fruit_id, supplier_id, quantity, price, total, purchase_date, remarktotal 由数量乘单价冗余存储sale销售流水id, fruit_id, quantity, price, total, sold_at, operator售出扣减库存用于毛利计算user后台登录账号id, username, password, rolerole 区分管理员与操作员字段类型的选择是要细说的地方。水果单价用 DECIMAL(10,2) 而不是 FLOAT因为浮点数对 decimal 的二进制存储会出现 0.1 加 0.2 不等于 0.3 这类误差期末答辩时一旦被问到「为什么不用 float」能说出这个小理由会加分。库存数量用 INT 足够不需要考虑负数负数应该在 Java 业务层提前拦截。日期字段统一用 DATETIME不要用 TIMESTAMP因为 MySQL 的 TIMESTAMP 在 2038 年存在溢出问题虽然水果摊撑不到那年但规范就是规范。按这个设计落库时注意一点fruit 表要建立 supplier_id 外键purchase 要同时外键到 fruit 和 suppliersale 只外键到 fruit。user 表和其他表不发生关系它只负责登录。2.3 使用 Navicat 创建数据库和表的具体操作在 Navicat 里建库不是随便点一下完事。连接上 MySQL 之后先新建数据库字符集选择utf8mb4排序规则选择utf8mb4_general_ci。为什么要用 utf8mb4 而不是 utf8原因是 utf8 在 MySQL 里最多占 3 字节存不了 emoji 表情和生僻字一旦插入 \ud83d\ude00 这类字符就会报Incorrect string value错误。虽然水果摊不太可能有人输入表情但这个选择能让后续中文写入彻底安全。建表时用 Navicat 的「表设计器」逐个字段输入名称、类型、长度。注意把 id 字段勾选「自动递增」千万不能手写主键值。所有表建完后在「外键」标签页把关联关系加上。一个我踩过的坑是大多数同学在建表时不区分物理外键和逻辑外键。物理外键就是真的用 FOREIGN KEY 语法创建数据库级约束逻辑外键则只靠字段名对应不加约束。期末大作业我强烈建议用物理外键。原因很简单MySQL 会自动维护引用完整性试图删除被 sale 表引用的 fruit 记录时会直接报错这个报错正好可以在答辩时演示「数据库的自我保护机制」。代价是插入数据时必须先插入父表再插子表这个顺序会带来一些不便但课设场景完全值得。对于合法性校验不要依赖 MySQL 的 CHECK 约束。MySQL 8.0.16 之后才真正强制执行 CHECK很多同学电脑上装的是 5.7 版本CHECK 会被解析但直接被忽略造成「明明设了条件还是能插进非法数据」的错觉。年龄、价格、库存这类校验统一放在 Java 的 Service 层做数据库只负责类型和外键。2.4 给外键列和查询频率高的列补索引索引不是越密越好水果摊这个量级的表索引加得太多反而拖慢插入。我一般强制要求三个索引fruit.supplier_id因为按供应商查水果是高频操作purchase.fruit_id 和 sale.fruit_id因为统计进货量和销量都要先关联sale.sold_at因为日报和周报按月分组必须走这个字段。在 Navicat 的字段设计页面里把上述字段的「索引」类型改为 Normal 即可普通索引足够不需要用唯一索引。逻辑依据是这些列都存在大量重复值唯一索引反而会导致插入失败。建立索引这一步不要拖到最后建表时就顺手做完后面写 SQL 查询的速度差异不大但 Navicat 的外键关联查询会顺滑得多。3. 在 Java 工程里把 JDBC 连接和 DAO 层站稳3.1 先约定工程结构再写代码很多期末大作业最后演变出一个几千行的 Main 类这种做法在答辩时非常吃亏。我习惯按标准的四层结构组织代码即使只是一个课设也值得这样分层fruit-shop/ ├── src/ │ ├── com/fruit/entity/ # 实体类对应五张表 │ ├── com/fruit/dao/ # 数据访问层只写 JDBC 和 SQL │ ├── com/fruit/service/ # 业务逻辑层处理事务和校验 │ ├── com/fruit/controller/ # Servlet 或控制台入口 │ ├── com/fruit/util/ # DBUtil 等工具类 │ └── db.properties # 数据库连接配置 ├── sql/ │ └── init.sql # 初始建表和测试数据 └── lib/ └── mysql-connector-java.jarmain 方法只留一个启动入口比如Main.execute()实际上执行 controller 里的菜单循环。实体类和表字段一一对应变量名用驼峰类型对齐数据库列类型。dao 层一个表一个类不交叉调用。service 层只要发现一个操作涉及多张表就由它来管事务。3.2 数据库连接用 DriverManager配置全放 db.properties连接 MySQL 有两种常见方式直接用DriverManager.getConnection或者引入 HikariCP 连接池。课设场景我强烈推荐前者原因是零第三方依赖、代码简洁、原理透明评委问起来你能把连接过程说清楚。连接池是性能优化的话题等进公司再研究不迟。以下是 DBUtil 的标准写法直接放进 util 包package com.fruit.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); // 加载驱动旧版本 MySQL 需要 Class.forName(com.mysql.jdbc.Driver) Class.forName(com.mysql.cj.jdbc.Driver); url props.getProperty(db.url); user props.getProperty(db.user); password props.getProperty(db.password); } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置文件读取失败 e.getMessage()); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, user, password); } }对应的 db.properties 文件db.urljdbc:mysql://localhost:3306/fruit_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue db.userroot db.passwordmysql666这段连接串里的参数每一个都有具体意义。characterEncodingutf8保证从 Java 写入 MySQL 的中文不乱码serverTimezoneAsia/Shanghai解决 JDBC 驱动与 MySQL 服务器时区不一致导致的报错useSSLfalse则跳过本地开发环境毫无必要的 SSL 握手。最后那个allowPublicKeyRetrievaltrue是 MySQL 8.0 的认证插件caching_sha2_password要求的不加会在第一次连接时报Public Key Retrieval is not allowed。这些参数建议背下来因为不管换哪台电脑配 MySQL 连接基本绕不开这几项。3.3 水果表 DAO 的编写范式DAO 层要避免拼接字符串 SQL用PreparedStatement。下面是水果表按关键字搜索并分页的 DAO 方法public ListFruit searchFruit(String keyword, int page, int pageSize) throws Exception { String sql SELECT f.id, f.name, f.category, f.unit, f.purchase_price, f.sale_price, f.stock, IFNULL(s.name, -) AS supplier_name FROM fruit f LEFT JOIN supplier s ON f.supplier_id s.id WHERE f.name LIKE ? OR f.category LIKE ? ORDER BY f.id LIMIT ? OFFSET ?; ListFruit list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // LIKE 通配符在参数里拼接不拼在 SQL 中 ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, pageSize); ps.setInt(4, (page - 1) * pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Fruit f new Fruit(); f.setId(rs.getInt(id)); f.setName(rs.getString(name)); // 省略其余 setter f.setSupplierName(rs.getString(supplier_name)); list.add(f); } } } return list; }这里有两个容易被问到的设计决策。一是使用LEFT JOIN而不是INNER JOIN目的是保证即使某个水果还没关联供应商也能在列表里显示出来供应商字段回退成横杠。如果用了 INNER JOIN这部分水果会被整体过滤掉加购时还看不到这个水果会显得产品逻辑缺陷。二是分页参数采用LIMIT ? OFFSET ?offset 在调用方计算好再传进来不要在 SQL 里写(page - 1) * pageSize。PreparedStatement 只认参数占位符SQL 中写任何表达式都不能帮你计算。3.4 购进入库必须做成事务事务边界在 Service 层进货这个动作包含两条数据变更向 purchase 插入一条进货流水同时把 fruit 表里对应的 stock 字段增加数量。如果只执行了第一步第二条语句执行前 MySQL 崩了就会造成「钱花了但库存没涨」的脏数据。因此这两步必须打包进一个事务。我在期末大作业里通常不加第三方框架直接用原生 JDBC 控制事务代码如下public boolean purchaseIn(Fruit fruit, Purchase purchase) throws Exception { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 第一步插入进货流水 PurchaseDao purchaseDao new PurchaseDao(); purchaseDao.insert(conn, purchase); // 第二步更新水果库存同一连接执行 FruitDao fruitDao new FruitDao(); fruitDao.increaseStock(conn, purchase.getFruitId(), purchase.getQuantity()); conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }做事务要注意两点。第一setAutoCommit(false)必须在拿到连接后立即执行如果先执行了一条查询再关闭自动提交之前的查询不会回滚。第二DAO 里的方法要接收外部传入的 Connection 而不是自己再调一次DBUtil.getConnection()。原因很简单事务是基于单条数据库连接的两个不同连接上的操作无法共享事务边界各提交各的。很多同学在这里栽跟头DAO 类内部各自获取连接最后发现异常时库存还是被改了。登录用户权限控制不要做得太复杂user 表里的 role 字段在登录时查出来操作员登录后前端隐藏「删除供应商」这类管理按钮即可。真正细颗粒度的 RBAC 权限模型超出水果摊的需求边界做多了反而被评委追问设计意图。4. 用 Navicat 把水果摊数据变成可查可视的管理面板4.1 从 ER 图反查表关系设计是否有误Navicat 一个容易被忽略的实用功能是把已经建好的表生成 ER 图。做法是点击菜单栏「查看」→「ER 图」或者直接在数据库名上右键选择「逆向数据库到模型」。注意这里是逆向而不是从模型建库。逆向出来之后Navicat 会把表之间的外键连线画出来我通常用三分钟审视这张图来发现设计问题。合理的 ER 图应该是supplier 上方顶着 fruitfruit 被 purchase 和 sale 分别连接形成典型的星型结构。如果发现 purchase 表直接连到了 supplier 而没有连 fruit或者出现了环形外键那就是建表时外键字段选错了回到表设计器调整。ER 图还有第二个用途期末答辩时直接截图放在 PPT 里的「数据库设计」章节比手画的方块图可信得多。顺便说一个 Navicat 连接到本地 MySQL 时的高频报错2059 - Authentication plugin caching_sha2_password cannot be loaded。这是 MySQL 8.0 默认认证插件和旧版 Navicat 的协议不兼容。如果你用的是 Navicat Premium 16 及以上版本通常没问题老版本的话要么升级客户端要么在 MySQL 里把用户的认证方式改回mysql_native_password命令是ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码;。4.2 把毛利和库存预警做进数据库视图视图是这个项目里炫技性价比最高的部分。它不占用额外存储空间只是把一段 SELECT 查询保存成一个虚拟表Java 代码直接用SELECT * FROM v_profit就能拿到按天按水果拆分的毛利明细。下面这段 SQL 是 MySQL 8.0 的合法写法利用窗口函数取最近一次进货价CREATE OR REPLACE VIEW v_profit AS SELECT DATE(s.sold_at) AS sale_date, f.name AS fruit_name, SUM(s.quantity) AS sale_qty, SUM(s.total) AS sale_amount, SUM(s.total - s.quantity * IFNULL(lp.price, f.purchase_price)) AS profit FROM sale s JOIN fruit f ON s.fruit_id f.id LEFT JOIN ( SELECT fruit_id, price, ROW_NUMBER() OVER (PARTITION BY fruit_id ORDER BY purchase_date DESC) AS rn FROM purchase ) lp ON lp.fruit_id f.id AND lp.rn 1 GROUP BY DATE(s.sold_at), f.name;这段 SQL 的查询逻辑是先把历史最近一次进货价找出来再和销量相乘得到成本最后用销售额减掉成本得到毛利。ROW_NUMBER() OVER (PARTITION BY fruit_id ORDER BY purchase_date DESC)的含义是按水果分组、按采购日期倒序编号编号为 1 的就是最新一次进货记录。如果你的 MySQL 是 5.7 版本不支持窗口函数就把子查询改成关联子查询或者用ORDER BY purchase_date DESC LIMIT 1的写法效果一致但效率略低课设规模无所谓。库存预警视图同样做成数据库对象CREATE OR REPLACE VIEW v_stock_warning AS SELECT f.id, f.name, f.quantity AS current_stock, f.safety_stock, CASE WHEN f.quantity f.safety_stock THEN 补货 WHEN f.quantity f.safety_stock * 1.2 THEN 偏低 ELSE 正常 END AS status FROM fruit f;操作路径是Navicat 中点击「视图」右键「新建视图」把 SQL 粘贴进编辑器保存后它就成为一个可查询对象。Java 端只需要SELECT * FROM v_stock_warning WHERE status 补货过期水果的库存逻辑一下子从几十行 JDBC 代码压缩到一行。4.3 在查询编辑器中建立常用查询模板Navicat 的查询编辑器支持把常用 SQL 保存为「查询文件」以后双击就能运行。我通常把下列查询都存成模板答辩时逐个运行给评委看比在 Java 控制台里来回敲命令高效得多场景SQL 要点按日期查销售流水SELECT * FROM sale WHERE DATE(sold_at) 2024-12-20查看滞销水果SELECT fruit_id, SUM(quantity) FROM sale GROUP BY fruit_id HAVING SUM(quantity) 5国庆期间销售额SELECT DATE(sold_at), SUM(total) FROM sale WHERE sold_at BETWEEN 2024-10-01 AND 2024-10-07 GROUP BY DATE(sold_at)与上月销量对比用MONTH(sold_at)分组后自关联或直接查两次导出对比按供应商统计采购总额SELECT supplier_id, SUM(total) FROM purchase GROUP BY supplier_id我遇到过不少同学在 Java 代码里硬编码这些统计 SQL每次改个日期都要重新编译。更好的做法是新建一个名为ReportDao的 DAO把这些 SQL 字符串集中维护日期参数用占位符传入。同时定期在 Navicat 查询编辑器里手动跑一遍同样的 SQL用于交叉验证 Java 的计算结果是否一致。两边的结果对不上时优先怀疑 Java 侧拼接条件时的边界问题比如日期有没有包含当天最后一秒。4.4 把查询结果导出成 Excel形成最直观的数据可视化Navicat 自带的数据导出功能在「查询结果」面板的右上角一个小表格图标点击后可以导出多种格式。我一般导出 Excel 378 格式把采购表、销售表、库存预警表分别导出后在 Excel 里做一张简单的数据透视表。这个方案适合期末大作业因为它展示了「Navicat 实现数据可视化管理」的完整链路而且比硬写一个 ECharts 页面快三倍。导出时有三个选项要格外注意。一是「包含列标题」必须勾选否则导出的 Excel 没有表头看起来非常业余。二是导出类型选择xlsx老版本的 Navicat 默认导出 xls2003 格式在最新 WPS 中打开偶尔报错。三是导出之前先在查询编辑器里把数据筛选到合适的规模不要一上来就全表导出万级以上的数据导出会卡住 Navicat。5. 联调时必查的 MySQL 与 Navicat 高频故障位5.1 Java 连接 MySQL 报错的快速定位表联调阶段遇见最多的是连接类错误我按频率高低整理了一张对照表遇到问题直接查表定位报错关键信息问题根因处理位置Communications link failure端口错、MySQL 未启动、防火墙拦截检查 3306 端口占用确认sudo systemctl status mysql状态Access denied for user rootlocalhost密码错误或 root 只允许本机登录核对 db.properties 密码改用rootlocalhost连库Unknown database fruit_shop数据库未创建或名称不匹配在 Navicat 确认数据库名与url中一致Public Key Retrieval is not allowedMySQL 8.0 认证插件导致的握手问题url 参数加allowPublicKeyRetrievaltrueThe server time zone value时区告警MySQL 全局时区未配置或配置失败url 参数加serverTimezoneAsia/Shanghai5.2 中文乱码的三个必查位置水果摊项目的所有文本都是中文乱码问题一旦出现按「存储 → 传输 → 展示」三段链路排查。存储层看表字符集在 Navicat 里右键表选择「设计表」确认字符集为utf8mb4而不是新建库时继承下来但被手工改掉的 latin1。传输层看连接参数有没有characterEncodingutf8注意参数名是 characterEncoding 而不是 charset。展示层看两处Java 控制台输出乱码的话检查运行时是否加了-Dfile.encodingUTF-8JSP 页面则在第一行声明% page contentTypetext/html;charsetUTF-8 %。这三层只要任何一层不是 UTF-8就会复现「存入正确读出来乱码」的问题。有一个区分定位的技巧在 Navicat 的查询编辑器里直接执行SELECT * FROM fruit如果 Navicat 里显示正常但 Java 输出乱码问题必然在传输层或代码展示层不需要再去改表结构。5.3 答辩前一晚必须跑完的数据备份与校验流程最后这一步是我每次帮人检查大作业时都会做的收尾操作。用 MySQL 自带的 mysqldump 在命令行完成整库备份不用借助 Navicat 的图形界面写成一个 bat 批处理文件双击即可mysqldump -uroot -pmysql666 fruit_shop D:\backup\fruit_shop_%date:~0,4%%date:~5,2%%date:~8,2%.sql mysql -uroot -pmysql666 fruit_shop -e SHOW TABLES; SELECT COUNT(*) FROM fruit;给两个参数备注%date:~0,4%%date:~5,2%%date:~8,2%在中文版 Windows 的日期格式下取年月日拼接成文件名按需调整偏移量-e参数允许直接在命令行执行 SQL适合在批处理里快速核对数据库状态。备份出来的 SQL 文件可以用 Navicat 的「运行 SQL 文件」功能还原到另一台机器这是期末答辩时把项目迁移到老师电脑上最稳的路径。所有功能跑通后把验证过的核心视图、备份脚本和 Navicat 导出的 Excel 截图归档到「答辩材料」目录里这套流程就完整闭环了。本文还有配套的精品资源点击获取