最近不少朋友在找Spring Boot练手项目,我翻到一个细节做得比较完整的超市货物管理系统案例,源码可以直接拉下来跑。这个系统覆盖了商品管理、库存管理、供应商管理和销售结算这些核心环节,用的是Spring Boot全家桶加MySQL的经典组合。如果你正准备做毕业设计,或者想从“照着教程敲”过渡到“看完整项目源码”,这个案例挺值得拆一拆的。我花了一个周末把源码完整过了一遍,顺手跑通了本地环境,下面从整体设计到具体实现,再到我踩过的坑,挨个给你说清楚。
这个系统给我的第一印象是:业务闭环做得比较完整,不是那种随便造两个CRUD页面就交差的demo。超市货物管理听着简单,真做起来涉及到的数据流转和权限控制一点也不少,尤其是库存和销售之间的联动,很容易写乱。而这个案例把这类核心逻辑处理得比较干净,代码结构也清楚,适合拿来做案例分析和二次开发。
1. 项目整体解读:它到底是一个什么系统
1.1 系统定位与适用人群
先明确一下,这个系统不是什么花哨的互联网产品,而是一个典型的企业级信息管理类项目。它解决的是超市日常运营中最头疼的几件事:商品资料太多了怎么维护、进货和销售数据太散怎么归集、库存数量总是对不上怎么办、供应商和采购记录怎么追溯。整个系统把这些线下靠表格和人工记忆完成的活儿,全部搬到线上,按照角色分配操作权限,每个人只能看到和自己相关的功能。
适用人群大致分三类。第一类是正在找毕业设计题目的学生,这个系统的业务规模和技术选型都合适,稍微改改就能作为课程设计或毕业论文的项目支撑;第二类是刚入行的Java开发,想通过一个完整项目把Spring Boot、MyBatis、MySQL这些技术串起来,而不是停留在写一堆增删改查接口的阶段;第三类是确实需要一个内部管理工具的小团队,比如社区超市、便利店连锁的运营人员,拿这套源码做二次开发,比从零开始省力太多。
我补充一句,这个源码的重点是“可运行、可学习、可扩展”,不是商业级成品。你拿来当学习资料或者项目原型完全没问题,但如果要直接部署到生产环境,数据库设计、权限粒度、日志审计这些方面还需要按实际业务加固。
1.2 为什么这个案例值得拆
市面上的CRUD Demo确实很多,但超市货物管理系统在业务上有一个特殊点:库存流转。入库、出库、退货、报损,每一类变更都会影响库存数量,也会影响成本核算。很多初学者写的库存系统只是一个“数量加减器”,进了一笔货就把库存加一下,卖了一单就减一下,根本没有考虑到单据来源、操作人、时间戳、库存快照这些信息。
这个案例做得比较好的地方,在于它把库存变更和历史记录分开了。商品表只管当前库存总量,每一次变动都会落到具体的进货单或者销售单里,通过关联字段可以追溯“这批货是什么时候进的”“谁卖的”“现在还剩多少”。这种设计思路虽然不复杂,但非常接近真实项目的做法。
另外它的代码分层也比较规矩。Controller、Service、Mapper三层分离,业务逻辑写在Service里,数据访问通过MyBatis的Mapper接口完成,页面用模板引擎渲染。这套结构对新手来说有三个好处:第一,代码路径清晰,想改哪个功能直接找对应层;第二,接口和页面解耦,后期想拆成前后端分离也容易;第三,符合主流Java项目的组织方式,面试时讲项目结构不会露怯。
2. 系统功能拆解:模块怎么设计才算合理
2.1 商品资料管理
商品是超市货物的基础数据,这个模块主要管三件事:商品分类、商品信息维护、商品状态切换。分类一般做两级就够了,比如“饮料”下面分“碳酸饮料”“茶饮”“果汁”,再多层级会增加维护成本。商品信息包括名称、条码、规格、单位、进价、售价、库存上限和库存下限,其中条码是唯一标识,进货和销售都靠它来关联。
这个模块里有个设计点值得注意:进价和售价分开存储。很多初学者喜欢只存一个价格,觉得超市卖东西就是进价加利润。但实际业务里,进价会随供应商报价浮动,售价会随促销活动调整,两个字段分开存才能支持后续的毛利统计。这个案例还引入了库存上下限的概念,低于下限时系统会给出补货提醒,这个逻辑在实际使用中非常实用。
操作层面,商品列表支持按分类筛选、按名称或条码搜索,新增和编辑走同一个表单页面。删除操作一般做成逻辑删除,也就是给商品加一个状态字段,而不是真删数据库记录,避免历史订单关联断掉。这个细节我特别强调一下,因为真实项目里物理删除几乎都是禁忌,一旦关联数据出现,删了主记录会让整条链路查不到源头。
2.2 库存与进货管理
库存模块是这个系统的核心,进货管理又是库存数据的源头。进货单通常包含以下几个要素:供应商、进货日期、商品明细列表、每种商品的进货数量、进货单价、整单金额。生成进货单之后,系统会自动把商品明细的数量累加到对应商品的库存字段里,同时生成一条库存变动记录。
这个案例的进货设计还考虑了退货场景。如果供应商送的货有质量问题,可以发起进货退货单,库存会做相应的扣减。退货逻辑虽然不复杂,但是很多初学者容易忽略,导致库存只增不减。加上退货之后,库存流转才算闭合。
库存盘点功能也值得一提。定期盘点发现账面库存和实际库存不一致时,通过盘点单调整库存数量,并记录差异原因。这个案例把盘点做成了一张独立的调整单据,而不是直接改商品表里的库存数字,好处是每次调整都有据可查。我见过太多直接改库存字段的做法,开始省事,后面出了问题根本说不清楚是谁改的、为什么改。
2.3 销售出库与结算
销售模块对应超市收银台和前台业务。和进货对称,销售这边有销售单、销售退货、流水查询三个核心功能。销售单生成时会校验商品库存是否充足,不足则提示补货,不允许超卖。这个校验我建议放在Service层做,而不是只在前端页面判断,用户直接调接口也能绕过页面校验。
销售退货则反过来,把商品数量加回库存,同时生成一笔负向的销售流水。有些系统把退货做成独立模块,这个案例是直接在销售单上做红冲,页面展示更直观,但底层逻辑其实是一样的。无论哪种方式,核心是保证库存变动和销售流水的一致性。
结算部分没有接第三方支付,只做记录和统计。毕竟超市货物管理系统的重点是货物和库存,不是收银软硬件对接。报表方面按日、按月统计销售额和销量排行,这些数据可以直接用SQL的聚合查询做,不需要额外引入复杂的报表引擎。对学习来说,看懂这些查询语句怎么写,比引入一个大而全的报表框架更有价值。
2.4 供应商与往来管理
供应商模块容易被人忽略,但在超市场景里它决定了进货成本和供货稳定性。这个案例维护了供应商的基本资料、联系人、联系方式、供货品类和合作状态,而进货单通过外键关联供应商,所以在查询“这个月从哪个供应商进了多少货”时效率很高。
有的系统会把供应商和商品做成多对多关系,因为一个供应商可以供多种商品,一种商品也可能有多个供应商。这个案例做的是简化版,采用了“一个商品默认关联一个主要供应商”的方式,降低理解成本。如果后续业务变复杂,可以再拆一张关联表。
往来管理这块,系统记录了每一次进货应付金额和每一次退货扣减金额,可以汇总出给供应商的累计应付。虽然没有做到财务系统那样的精细对账,但作为超市管理系统的一个子功能已经够用了。对于新手来说,从这里理解“单据驱动数据”的设计思想,比单纯看表结构要深刻得多。
2.5 用户与权限控制
权限控制是管理系统的安全底线。这个案例把用户分成了管理员、店长、收银员、库管员等角色,不同角色看到的功能菜单不一样。比如收银员只有销售开单和流水查询权限,库管员能操作进货、退货和盘点,管理员则拥有全部功能。
落地方式比较直接:用户表关联角色表,角色表关联菜单表,用户在登录时加载自己角色对应的菜单,然后把角色标识存到会话里,在请求进入Controller之前做权限拦截。这种基于角色的访问控制模型理解了以后,换到任何框架都能快速迁移。
我特别说一下权限拦截的位置。这个案例用的是Spring Boot拦截器,通过HandlerInterceptor判断会话中是否有登录用户,以及当前用户角色是否允许访问某个路径。学习的时候要留意拦截器注册的代码,比如哪些路径放行、哪些路径拦截、静态资源怎么排除,这些细节在真实项目中经常因为配置不当导致页面进不去或者接口被绕过。
3. 技术栈与数据库设计解析
3.1 技术栈选型
这套系统的技术栈是比较典型的Spring Boot单体应用组合。后端用Spring Boot作为基础框架,数据持久层用MyBatis,数据库用MySQL,前端页面使用模板引擎渲染。下面用一个表格对照说明选型原因:
| 技术组件 | 本案例用法 | 为什么这么选 |
|---|---|---|
| Spring Boot 2.x | 提供自动配置、内嵌容器、依赖管理 | 降低环境配置成本,快速启动项目 |
| MyBatis | 编写Mapper接口和XML映射SQL | 灵活控制SQL,适合复杂查询和动态SQL |
| MySQL 5.7+ | 存储业务数据 | 开源免费,中小型项目首选,资料丰富 |
| Thymeleaf | 渲染后台管理页面 | 与Spring Boot集成度好,语法直观 |
| Bootstrap | 页面样式和布局 | 快速搭出干净的后台界面,不需要前端工程化 |
| Maven | 依赖管理和项目构建 | Java项目事实标准,换环境也能快速构建 |
这套选型不一定是最新的,但胜在稳定和资料多。如果你用Spring Boot 3.x去跑这套源码,可能会遇到Jakarta命名空间替换的问题,那是比较大的改动。我建议先按源码版本环境来跑,跑通了再考虑升级。
3.2 数据库表结构规划
数据库设计是这个案例的精华之一,核心表大概有这些:用户表、角色表、菜单表、角色菜单关联表、商品分类表、商品表、供应商表、进货单主表、进货单明细表、销售单主表、销售单明细表、库存变动记录表。表与表之间通过外键或逻辑关联字段连接,整体遵循第三范式,冗余字段很少。
举个具体例子,进货单会拆成主表和明细表两张。主表存供应商ID、进货日期、整单金额、操作人,明细表存商品ID、进货数量、进货单价、小计金额。为什么拆两张?因为一张进货单包含多个商品,如果只存一张表,要么数据冗余严重,要么无法表达“一张单对应多行明细”的自然关系。这种主从表结构是进销存系统的通用模式,学会这个对理解订单类系统帮助很大。
商品表的库存字段保存当前数量,库存变动记录表保存每一次增加或减少的明细。两个表的分工不一样:商品表是结果,变动记录表是过程。设计系统时如果只保留结果,后面想分析进货趋势、销售趋势、损耗原因,会发现数据根本不够;把过程留下来,各种统计报表才有源头。
3.3 关键表关系设计思路
商品表和库存变动记录表是一对多关系:一个商品对应多条变动记录。进货单主表和明细表是一对多关系:一个主表单对应多条商品明细。供应商和进货单也是一对多关系,但进货单明细通过商品ID关联商品表,不直接关联供应商,供应商信息从主表取。这种表关系设计在实际查询时非常顺手。
有一个容易纠结的点是:商品表要不要直接存供应商ID。如果存了,查询商品列表时可以一次性带出供应商名称,省去关联查询,但一个商品如果有多个供应商就表达不了。这个案例选择不存,而是通过最新进货单去推导默认供应商。这个取舍思路值得学习:在表设计阶段就要想清楚业务边界,不要为了查询方便强行加冗余字段。
给库存变动记录表加索引时也要动点脑筋。查询条件通常是商品ID加时间范围,所以联合索引(商品ID、变动时间)能显著提升查询性能。金额字段建议用DECIMAL类型而不是FLOAT,账务相关数据对精度要求极高,浮点数在累加时会出误差。这些细节一眼看上去没什么,但却是实际项目里最容易埋坑的地方。
4. 环境搭建与源码运行实操
4.1 本地环境准备
很多初学者卡在第一步:源码拿到了,不知道怎么跑起来。我先把基础环境清单列出来。JDK装1.8版本,Maven用3.6以上,MySQL装5.7或8.0都可以,开发工具用IDEA或者Eclipse都行。数据库客户端我习惯用Navicat,但用命令行也完全没问题。
确认环境没问题后,把源码导入开发工具。以IDEA为例,选择Import Project,然后选中pom.xml,让IDEA以Maven项目方式加载,它会自动下载依赖。第一次导入可能比较慢,因为Maven要拉Spring Boot相关的依赖包,如果网络状况一般,等上十几分钟是正常的。
我强调一下JDK版本问题。Spring Boot 2.x对JDK 8、11都兼容,但有些新版本IDEA默认带的JDK是17或21,直接编译旧项目会出现一些问题。我建议在Project Structure里把Project SDK明确设置为1.8,同时在Settings里把Java Compiler的字节码版本也改成1.8,保持一致就不会出幺蛾子。
4.2 配置文件与数据库初始化
源码里通常自带一个SQL脚本文件,用来创建数据库和初始化数据。先打开MySQL,新建一个数据库,名字最好和配置文件里的库名保持一致,然后导入脚本。导入完成后,确认一下表是否都建好了,再检查初始数据,尤其是管理员账号的用户名和密码在哪个表里。
接下来打开resources目录下的application.yml或application.properties,重点看四样配置:数据源URL、数据库用户名、数据库密码、端口号。数据源URL一般长这样:jdbc:mysql://localhost:3306/库名?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,注意serverTimezone这个参数在MySQL 8.0里很容易忘,加了才能避免时区报错。
修改配置时有一个我踩过好几次的坑:密码里如果有特殊字符,比如@、#、$,YAML文件里必须用引号包起来,不然解析会出错。还有用户名如果写成了root但MySQL里实际是其他用户,启动报错的时候不要怀疑是代码问题,先用客户端工具测试连接,确认账号密码能连上数据库再排查其他。
4.3 启动项目与管理员登录
配置改好后,找到项目的主启动类,类名一般长这样:XxxApplication,直接右键运行。控制台输出Spring Boot启动日志后,看到Tomcat started on port(s): 8080这行字样,说明项目已经起来了。
打开浏览器访问localhost:8080,正常情况下应该能看到登录页。用SQL脚本里预设的管理员账号登录,如果登录后页面空白或者跳转有问题,优先看控制台有没有异常堆栈,常见的模板路径错误就是Thymeleaf找不到对应的HTML文件,检查一下Controller返回的视图名和templates目录下的文件名是否一致。
还有一个常见问题是静态资源加载不出来,页面长得很难看。检查有没有配置静态资源放行规则,因为拦截器可能会拦截.css和.js请求,需要在配置类里对静态资源路径做exclude。这种问题新手特别容易遇到,后面我也会在问题排查部分再详细展开。
5. 核心代码实现解析
5.1 项目目录结构
拿到源码后先别急着跑,先把目录结构理一遍。标准的Spring Boot工程结构分为controller、service、mapper、entity或者pojo、config、common几个包,resources下放mapper的XML文件、静态资源和页面模板。熟悉了这种结构后,改任何功能都知道去哪里找。
我强烈建议拿到源码后先画一个功能清单和Controller类清单的对照表。比如商品管理的URL以/product开头,库存管理的URL以/stock开头,销售模块以/sale开头,把这类对应关系整理出来,再读代码就会快很多。读懂一个模块的完整链路后,其他模块基本就是复制路径找对应类。
这个案例里还有一些通用类值得留意,比如统一返回结果的Result类、异常处理类、分页工具类。这些通用组件虽然代码不多,但体现了工程化思维。后续自己写项目时完全可以照搬这套工具类的写法,让接口返回值保持统一格式,前端解析就方便得多。
5.2 实体类与数据访问层
实体类对应数据库表,字段一般和表字段一一对应。以商品实体为例,除了id、name、barcode这些基本信息外,还会有一个库存字段和一个状态字段。在Java里用BigDecimal表示金额字段,用LocalDateTime表示时间字段,这些类型在MyBatis里都有对应的类型处理器。
MyBatis的Mapper接口只定义方法名,真正的SQL写在XML文件里。看XML文件时要重点看动态SQL的使用,比如商品列表查询经常需要根据分类ID、名称关键词、状态条件动态拼接where语句,MyBatis的if标签就是干这个用的。这种写法比字符串拼接SQL安全得多,也能复用查询条件。
一个我见过的典型问题是:XML文件里的resultType和resultMap用混了。简单查询用resultType直接映射实体类没问题,但多表联查有嵌套对象时就要用resultMap定义映射关系。这个案例的商品分页查询用了多表联查,所以它使用了resultMap来映射商品和分类的关联关系。
5.3 业务服务层
Service层是业务逻辑的集中地。以进货为例,Service层做的事远远不止插入一条数据:要计算整单金额,要逐条检查商品是否存在,要更新商品库存,要写库存变动记录,还要将进货单主表和明细表的数据同时保存。这些操作必须放在一个事务里,任何一个环节失败,所有数据变更都要回滚。
Spring Boot用@Transactional注解声明事务,放在方法上即可。这个案例的进货方法上肯定有这个注解,销售方法上同样有。你可以做个实验:在更新库存之后手动抛一个运行时异常,看数据库里的数据是不是没有半更新状态,这样可以直观理解事务的作用。没有事务的话,进货单保存了但库存没加上,账面就会对不上。
库存扣减还有一个并发问题:两台收银机同时卖同一个商品,最后一条商品怎么判断库存是否够。这个案例用的是先查询再更新的方式,也就是查到库存后判断一下,够就更新扣减。严格来说这里有并发隐患,两个请求同时读到库存为1,都判断够,都执行扣减,库存就变成-1了。作为进阶练习,你可以改成UPDATE语句里带上stock >= 购买数量这个条件,数据库层面加锁,就能解决超卖。
5.4 控制器层与页面渲染
Controller层负责接收HTTP请求、调用Service、把结果返回给页面或JSON接口。表单页面提交时,Spring Boot会自动把请求参数绑定到实体对象上,但要注意日期格式的绑定问题。前端传来的日期是字符串,默认情况下可能转换失败,需要加@DateTimeFormat注解。
页面模板使用Thymeleaf的话,最常见的两个语法是th:each循环表格行和th:href拼URL路径。${...}表达式直接取后端放大的Model属性,配合th:if做条件判断也很方便。如果之前只写过纯HTML和JS的页面,第一次看Thymeleaf可能会觉得混着标签很奇怪,但适应之后会发现比写JS模板拼接字符串省事很多。
表单提交后的页面跳转,我看案例里用了redirect:,也就是重定向。为什么要重定向而不是直接返回一个视图?因为直接返回视图的话,用户刷新页面时会重复提交表单,导致重复插入数据。重定向可以让浏览器发起一次全新的GET请求,这个细节体现了防重复提交的意识。
6. 常见问题与排查实录
6.1 数据库连接失败
启动时报错最常见的就两类:Access denied for user和Communications link failure。前者是用户名密码错误,检查配置文件里的数据源账号和MySQL实际账号是否一致;后者通常是地址写错了,端口不是3306,或者数据库服务没启动。用数据库客户端先连一下,能连上就说明问题在Spring配置这一侧。
时区报错The server time zone value是MySQL 8.0特有的,在数据库连接串后面加上serverTimezone=Asia/Shanghai就能解决。还有一类特殊情况是MySQL版本用了8.0但驱动包版本低,遇到SSL连接报错可以加useSSL=false。这类参数我在前面提到的连接串里已经给全了,直接抄就行。
6.2 端口被占用
Tomcat启动失败,报Port 8080 was already in use,说明8080被其他程序占了。Windows下用netstat -ano | findstr 8080查占用端口的进程PID,然后到任务管理器里找到对应进程结束掉。也可以直接在配置文件里把端口改成8081或者9090,只要不冲突都行。
改端口的时候顺便看一下上下文路径。有的项目配置了server.servlet.context-path,访问地址会多一层前缀,比如localhost:8080/shop/,如果不知道这个配置,找半天登录页都找不到。路径配置偶尔会在application.yml里被注释掉,建议把整个配置文件从头到尾扫一遍,别只看数据库部分。
6.3 Maven依赖下载慢或失败
导入项目时IDEA右下角转个不停,依赖却迟迟下不完,这大概率是Maven源的问题。打开Maven的settings.xml,把镜像地址改成国内镜像,然后重新加载项目。依赖下载失败还有一种情况是版本冲突,看IDEA的报错信息是哪个依赖解析不了,网上搜一下对应版本的Maven坐标就能解决。
如果项目能编译但运行时提示NoClassDefFoundError,多半是某个依赖没有打进去。在IDEA里执行Maven的clean和package,把整个项目重新构建一遍,再运行主类。这个操作能解决很多奇奇怪怪的编译期和运行期不一致的问题。
6.4 前端页面乱码与静态资源404
页面显示中文乱码,先检查浏览器页面编码是不是UTF-8,再检查数据库表的字符集是否为utf8mb4,最后看连接串有没有characterEncoding=utf8。三个地方都对了中文就不会乱。页面打开没样式,基本是静态资源被拦截器拦截了,需要在拦截器注册处放行/css、/js、/images路径。
访问某个功能页面时报Whitelabel Error Page,说明Controller处理过程中抛了异常。这时候要看IDEA控制台的完整异常栈,不要只看页面上的那一行错误。异常栈会直接告诉你是哪一行代码出的问题,是空指针、SQL语法错误还是参数类型转换失败。
我整理了一张问题速查表,方便你排查时快速对照:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 启动报Access denied | 数据库账号密码错误 | 用客户端测试连接,改配置 |
| 启动报时区错误 | MySQL 8.0时区未设置 | 连接串加serverTimezone参数 |
| 页面无样式 | 静态资源被拦截 | 放行/css、/js等路径 |
| 中文乱码 | 编码不统一 | 统一UTF-8或utf8mb4 |
| 端口占用 | 本地8080被占 | 改端口或结束占用进程 |
| 缓存不生效 | 依赖版本冲突 | 执行clean和package重建 |
我在实际跑这套源码时踩过最隐蔽的一个坑是:登录成功后跳转页面一直循环重定向。排查到最后发现是登录成功往Session里放了用户对象,但拦截器取Session用的key和存入时不一致,导致每个请求都认为未登录。如果你遇到类似情况,优先检查Session的key前后是否一致,这是初学者特别容易忽略的细节。
还有个小建议,拿到源码后先把application.yml里的数据库配置单独复制一份保存,因为重新初始化数据库后经常会需要再改一次配置。另外跑通第一遍之后记得导出一次数据库,后续二次开发改乱了大不了直接恢复初始数据,省得反复导脚本。
这个系统整体跑下来,给我的感觉是麻雀虽小但五脏俱全。它最值得学习的点不在某个高深技术,而在于把超市货物流转的每一笔数据都管理得有据可循。如果你能把进货、销售、退货、盘点这套库存变动逻辑完全理顺,再去看其他任何进销存类系统都会觉得轻松很多。源码在手边,下一步就是用起来,哪怕每天改一个小功能,也比只看不练强得多。