水果管理系统这个题目,我见过太多次了,也帮不少同学审过代码。它是计算机毕业设计里的“稳选题”,业务链路刚好卡在一个很舒服的位置:比图书管理复杂,又比电商平台简单,既有常规的增删改查,又有库存预警、订单流转、销售统计这些能写进论文的亮点。基于Spring Boot的水果销售与库存管理系统,本质上就是把水果店的进货、上架、售卖、盘点、结算这些日常动作搬到线上,让老板打开页面就能看到今天卖了多少、还剩多少、哪些水果该补货了。这篇内容适合两类人:一类是正在写毕业论文、想快速搞清楚怎么搭和怎么讲的毕业生,另一类是拿完整项目练手、想把Spring Boot全家桶串起来的开发者。我按实际开发顺序来讲,重点放在能直接落地的方案和踩过的坑上。
1. 项目定位与整体设计思路
1.1 先认清业务主线,功能模块自然就出来了
很多同学一上来就急着建工程、写登录页,结果模块写到一半全乱套。我的习惯是先模拟水果店的一天:早上进货,把苹果、香蕉往货架摆,登记成本和数量;中午顾客来买,选了几样水果,结账;晚上打烊前核对剩余库存,算今天的营业额,看看哪些水果快卖完要补货。这条线捋清楚,系统的功能模块就顺理成章了。
站在这个角度,系统至少需要拆成这些模块:
- 系统登录与员工管理:区分管理员和普通员工,控制谁能进后台。
- 水果分类与水果档案:分类是商品的“货架”,档案是商品的基础信息。
- 采购进货模块:每次进货登记数量、价格、供应商,库存自动增加。
- 库存台账与预警:实时展示当前库存,库存低于阈值时给出提示。
- 销售订单模块:选商品、加购物车、结算下单,库存自动扣减。
- 客户管理模块:记录客户姓名、电话、积分,为后续统计做铺垫。
- 报表统计模块:按天、按月汇总营业额,做销售趋势图和商品销量排名。
按这个思路拆,答辩时导师问“为什么设计这个模块”,你可以直接说它是业务里的哪一步,而不是按框架功能硬凑。系统最终要解决的就是两个字:台账。过去水果店用本子记账,现在把账本变成数据库,把人工算账变成系统自动算。
1.2 技术选型这样定,稳中带亮点
技术栈不是越新越好,关键是你能讲清楚。我做这类项目推荐一套组合:Spring Boot做基础框架,MyBatis-Plus做持久层,MySQL存数据,Thymeleaf结合Bootstrap做页面,图表用ECharts。这个组合很现实,理由如下。
Spring Boot把Tomcat、依赖管理、配置都封装好了,不用在环境上折腾太久。MyBatis-Plus能把单表增删改查压缩到几乎不写SQL,省下来的时间去设计订单和库存这种核心业务。Thymeleaf加Bootstrap是前后端不分离的方案,比Vue分离少写一层接口文档和跨域处理,演示时也少几个翻车点。那是不是完全不能用Vue?不是不能用,而是加了之后工作量会翻倍,答辩时还容易被打断追问“前端token过期怎么处理”,如果准备不充分反而扣分。
如果你想把技术亮点做足,可以在认证上引入Spring Security,或者在热点数据上引入Redis做缓存。但前提是你能把原理讲透,否则就保持简单。毕业设计评分看的不是技术数量,而是你对系统的理解深度。
1.3 项目目录结构与开发推进节奏
包结构建议按职责分,清晰好解释:
com.example.fruit ├── controller // 控制层 ├── service // 业务层 ├── mapper // 数据访问层 ├── entity // 实体类 ├── config // 配置类,如拦截器、资源映射 └── common // 统一返回结果、异常处理、工具类开发节奏建议按“登录到分类,再到商品,再到库存,然后是进货和销售,最后做统计和客户”的顺序推进。前几个模块是在练基本功,中间两个是核心业务,后面是加分项。先跑通一条主链路再补页面,千万不要一上来就调图表,图表永远是最后锦上添花的东西。
2. 数据库设计:先把表立住
2.1 核心表的字段这样定
数据库是整个系统里最重要的一层,表结构歪了,后面写再多代码都别扭。按角色和业务拆,核心表一共七张:管理员表、水果分类表、水果信息表、采购进货表、销售订单表、订单明细表、客户表。
其中水果信息表里直接放库存数量和预警阈值。对于水果店铺这种单人作业场景,库存和商品放同一张表就够了,查询简单,也够用。四张基础表的字段设计如下:
| 表名 | 主要字段 | 设计要点 |
|---|---|---|
| admin_user | id, username, password, real_name, role, status, create_time | username加唯一索引,password存加盐后的密文,role区分管理员与员工 |
| fruit_category | id, name, sort_order, deleted, create_time | name加唯一索引,sort_order控制分类展示顺序 |
| fruit | id, category_id, name, price, unit, stock, warning_stock, image, status, deleted, create_time | category_id做普通索引,stock和warning_stock是库存预警核心 |
| customer | id, name, phone, points, remark, create_time | phone可加普通索引,points记录消费积分 |
三张业务表的字段设计如下:
| 表名 | 主要字段 | 设计要点 |
|---|---|---|
| purchase_record | id, fruit_id, fruit_name, price, num, amount, supplier, purchase_date, remark, create_time | fruit_id关联水果表,amount由price乘num得出 |
| sale_order | id, order_no, customer_id, total_amount, pay_type, status, remark, create_time | order_no加唯一索引,status标记订单状态,create_time加索引用于统计 |
| sale_order_item | id, order_id, fruit_id, fruit_name, price, num, amount | order_id加索引,fruit_name冗余一份防止水果改名后历史订单显示异常 |
订单明细冗余水果名称是有意为之。如果以后水果改名或者被删除,历史订单仍然能显示当时的商品名,这是企业应用里的通用做法,答辩时提这个细节会很加分。
2.2 三个容易想当然的设计细节
第一,金额字段必须用decimal,不能用float或double。水果按斤卖,价格可能到小数点后两位,浮点数在累加时会出现0.30000000000000004这种误差。第二,重量字段要用decimal(10,3),不是int,因为可能出现0.5斤、1.25斤这种数量。第三,逻辑删除要统一:所有需要保留历史的表都加deleted字段。
物理外键建议不要加。很多同学喜欢在表上建外键约束,结果删除分类时提示有商品关联,删除不了,很尴尬。正确做法是不建物理外键,在查询时用JOIN关联,数据一致性通过代码保证。删除分类前先判断分类下有没有启用中的商品,有就提示“该分类下存在商品,不能删除”,体验更好。
索引不是越多越好,但下面三处值得加:分类表的name、水果表的category_id、订单表的create_time和order_no。这些是查询和统计的高频字段。
2.3 先塞一份演示数据再开写
建完表先别急着写业务,把演示数据塞进去,后面每一步都有东西可看。分类给四类:苹果类、柑橘类、时令瓜果、热带水果。水果给十几个品种,其中一两个设置成低于预警值,方便演示预警功能。商品售价要有零头,比如苹果8.5元每斤,订单统计出来才真实。
INSERT INTO fruit_category(name, sort_order) VALUES ('苹果类', 1), ('柑橘类', 2), ('时令瓜果', 3), ('热带水果', 4); INSERT INTO fruit(category_id, name, price, unit, stock, warning_stock, status) VALUES (1, '红富士', 8.50, '斤', 20.000, 5.000, 1), (2, '赣南脐橙', 6.80, '斤', 2.000, 5.000, 1), (4, '海南香蕉', 3.50, '斤', 30.000, 8.000, 1);上面第二条数据库存已经低于预警值,登录系统后首页预警区域就能直接看到效果。建议再多插几条不同日期的销售订单,图表统计时数据才不会空。
3. 核心功能实现:把下单和库存这条主链路跑通
3.1 登录认证与拦截器,5分钟搭完
登录是系统第一道门。毕业设计用Session加拦截器足够,实现简单不容易出错。先写一个登录拦截器,判断Session里有没有当前用户,没有就重定向到登录页。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("/login"); return false; } return true; } }然后把它注册进配置类,放行登录页和静态资源。
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/css/**", "/js/**", "/images/**"); } }密码不能存明文,至少做一层MD5加盐处理,或者直接用BCrypt。答辩时被问到“密码怎么保证安全”,能答出“数据库存的是哈希值,不是原文,登录时比对哈希结果”,这个回答就够了。
3.2 图片上传与资源映射,避开丢图大坑
水果档案里最好带图片,不然列表页太素。图片上传最大的坑是把文件写进了项目源码目录,重新部署后图片全丢。推荐把图片统一放到项目目录外的一个uploads文件夹,再通过资源映射让页面访问。
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String path = System.getProperty("user.dir") + "/upload/"; registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + path); }注意路径要以斜杠结尾。上传文件时用UUID或者时间戳重命名文件名,不要直接用用户上传的原始文件名,避免中文文件名和路径分隔符引发的问题。页面显示时直接用“/upload/文件名”访问即可。
3.3 库存台账与预警,一条SQL把缺货清单带出来
库存预警是毕业设计里最值得讲的功能之一,实现起来并不复杂。水果表里已经有stock和warning_stock两个字段,缺货就是库存小于等于预警值。列表查询里加一个过滤条件即可。
SELECT id, name, price, unit, stock, warning_stock FROM fruit WHERE deleted = 0 AND status = 1 AND stock <= warning_stock ORDER BY stock;前端把预警状态的商品标红,首页统计缺货数量。进货流程要做好事务:采购进货表新增一条记录,同时更新fruit表的stock字段,stock等于原库存加本次进货数量。如果中间失败,两个操作都要撤回。
预警值设计成商品自带的字段,而不是写死,这样不同水果可以有不同的补货策略,比如香蕉卖得快预警值设高,榴莲卖得慢预警值设低。答辩时解释这个设计,比简单写一个固定阈值更有说服力。
3.4 销售下单与库存扣减,这段代码是项目亮点
整个系统最拿得出手的代码就是创建订单这一段。购物车结算时,后端要做的事情看着不多,但步骤必须严格:
- 生成唯一订单号
- 插入订单主表
- 循环插入每条订单明细
- 逐条扣减库存
这四步必须在同一个事务里执行,否则会出现库存扣了订单没生成,或者订单生成了库存没扣的情况。
@Transactional(rollbackFor = Exception.class) public SaleOrder createOrder(OrderCreateDTO dto) { String orderNo = "SO" + System.currentTimeMillis() + RandomUtil.randomNumbers(4); SaleOrder order = new SaleOrder(); order.setOrderNo(orderNo); order.setCustomerId(dto.getCustomerId()); order.setStatus(0); saleOrderMapper.insert(order); for (OrderItemDTO item : dto.getItems()) { int rows = fruitMapper.deductStock(item.getFruitId(), item.getNum()); if (rows == 0) { throw new RuntimeException("库存不足:" + item.getFruitName()); } SaleOrderItem detail = new SaleOrderItem(); detail.setOrderId(order.getId()); detail.setFruitId(item.getFruitId()); detail.setFruitName(item.getFruitName()); detail.setPrice(item.getPrice()); detail.setNum(item.getNum()); detail.setAmount(item.getPrice().multiply(item.getNum())); saleOrderItemMapper.insert(detail); } return order; }库存扣减的SQL是防超卖的关键。
@Update("UPDATE fruit SET stock = stock - #{num} WHERE id = #{id} AND stock >= #{num}") int deductStock(@Param("id") Integer id, @Param("num") BigDecimal num);这行SQL的逻辑在于:数据库更新时锁住这一行,并且要求库存大于等于购买数量才允许更新。受影响行数为0就说明库存不够,直接抛出异常让事务回滚。这比先查库存再判断的写法安全得多,并发场景下也不容易超卖。
3.5 销售统计与图表,把数据变成经营看板
统计模块最能体现系统价值。让老板看到今天卖了多少、最近七天的走势、哪个水果卖得好,比表格直观得多。报表数据不用单独建表,全是SQL聚合查询。
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS order_count, SUM(total_amount) AS total_amount FROM sale_order WHERE status IN (1, 2) AND create_time BETWEEN #{start} AND #{end} GROUP BY day ORDER BY day;如果订单状态里0代表待支付、1代表已支付、2代表已完成、3代表已取消,统计营业额时要把已取消的过滤掉。分类占比按sale_order_item里的fruit_name分组求和,销量Top5按照每个商品的销售数量排序。前端接ECharts,后端返回List结构,页面用Ajax拉数据再配置option。
时间参数要统一格式。MySQL的create_time是datetime类型,Java传参时注意LocalDateTime和字符串的转换,否则很容易查不到数据。
4. 常见问题与避坑技巧
4.1 事务不生效,最常见也最致命
事务失效是这类项目里出现频率最高的问题,而且症状隐蔽。一个典型场景是:调用创建订单方法,里面库存扣减失败,但程序没有报错,事务也没有回滚,最后订单生成了库存却没扣。
原因通常是这几个。第一,类内部方法自调用。同一个类里的方法直接调用本类方法,不会经过Spring代理,@Transactional注解不生效。解决方法:把下单逻辑放到另一个类里,或者在当前类注入自己的Bean再调用。第二,异常被try-catch吞掉。事务只在抛出运行时异常时回滚,你在方法内部catch住异常不重新抛出来,事务就认为一切正常。第三,方法不是public。Spring默认只能代理public方法,私有方法上的事务注解无效。第四,数据库引擎不是InnoDB。MyISAM引擎不支持事务,检查数据库表引擎配置。
排查时最笨但最有效的办法:在事务方法里故意抛一个运行时异常,看数据库是否回滚。能回滚说明配置正常,不能就要从上面四个原因逐一查。
4.2 日期时间与格式,一串小问题全在这
接口返回的LocalDateTime字段,前端经常显示成一段数字或者时间少8小时。解决办法是在字段上加格式化注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;MySQL连接串记得加上serverTimezone=Asia/Shanghai,否则在部分环境下会出现时区偏移。写查询SQL时,日期比较用BETWEEN要谨慎处理边界,比如统计某天数据最好的方式是create_time大于等于当天零点,小于第二天零点,而不是单纯套字符串。
4.3 端口占用、图片丢失与删除受限的应对
端口占用是个高频问题。修改application.yml里的server.port换一个端口就行,不要反复强行kill进程。图片上传后访问404,检查addResourceLocations路径结尾有没有斜杠,以及上传的文件名是否带异常字符。删除分类提示有外键关联,多半是建表时加了物理外键,方案是去掉物理外键改用逻辑删除,删除前先查分类下是否存在商品。
金额计算出现精度问题,把float和double换成BigDecimal。数据库里decimal字段映射到Java端用BigDecimal接收,业务计算也用BigDecimal,不要用字符串拼接。
5. 部署、测试与答辩通关清单
5.1 打包部署与数据备份
到了验收阶段,把项目打成jar丢到服务器上是最省事的。本地执行打包命令:
mvn clean package -DskipTests服务器装好JDK和MySQL。Spring Boot 2.x对应JDK8,Spring Boot 3.x需要JDK17,版本注意对应上。上传jar和数据库脚本,启动命令:
nohup java -jar fruit-system.jar > log.log 2>&1 &数据库备份用mysqldump定时任务,每天凌晨备份一次,保留最近一周的备份文件。答辩前至少完整部署演练一遍,不要等到那天才现找问题。
5.2 测试用例设计,直接写进论文附录
系统做完一定要有一份测试记录,论文里能直接用。按模块写用例,每个用例包含操作步骤、预期结果、实际结果。这份测试表建议放在论文附录,纸质档案和电子版各留一份。
| 模块 | 测试场景 | 操作步骤 | 预期结果 |
|---|---|---|---|
| 登录 | 密码错误 | 输入错误密码登录 | 提示用户名或密码错误,不进入后台 |
| 分类 | 删除有商品的分类 | 删除苹果分类 | 提示该分类下存在商品,不能删除 |
| 库存 | 预警触发 | 将某水果库存改为低于预警值 | 首页预警列表出现该商品 |
| 订单 | 下单减库存 | 购买3斤苹果后查看库存 | 苹果库存对应减少3斤 |
| 统计 | 营业额过滤 | 查询包含已取消订单的数据 | 营业额不包含取消订单 |
这份案例表不用太长,覆盖核心功能即可。每个模块选两个典型场景,整份表格控制在两页以内。
5.3 答辩演示的建议顺序
答辩时间通常很短,演示别从登录页开始一个功能一个功能点。建议顺序:先讲业务背景,一句话带出系统解决的核心痛点;然后演示库存预警,这是视觉冲击力最强的功能,列表标红很醒目;接着演示从下单到库存扣减的完整流程,顺势引出事务和防超卖SQL的设计;最后切到统计图表,证明系统能对经营真正有帮助。
登录、员工管理这些基础模块,一句话带过就行。技术原理问题要提前准备:为什么库存和商品放一张表,为什么订单明细要冗余商品名称,为什么库存扣减用条件更新而不是先查后改。这三个问题都能答顺,项目答辩基本就稳了。
最后说句实在话:这类管理系统,真正拉开差距的不是界面多漂亮,而是库存和订单这条主链路有没有做严谨。把表结构建清楚、事务写对、库存扣减的条件更新用上,你的项目就不只是“能跑”,而是“能讲”。选了这个题目的同学,按这个顺序推一遍,心里基本就有底了。