ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

小型超市商品管理系统设计与实现:Spring Boot + MyBatis 进销存实战

小型超市商品管理系统设计与实现:Spring Boot + MyBatis 进销存实战 “小型超市商品管理系统”这个题目在计算机专业的课程设计、毕业设计里属于出镜率最高的那一档。以前我也觉得这种系统太简单直到认真把一份免费源码从头到尾撸了一遍才发现里面藏了不少值得展开的知识点比如事务边界、库存扣减、SQL 聚合统计、参数绑定这些。今天就把这个项目的设计与实现过程完整拆一遍重点是那些你自己写的时候一定会踩的坑以及拿到源码之后怎么落地跑起来。这套小型超市商品管理系统的源码本质上是一个典型的进销存应用核心就干三件事管商品、管进货、管销售。系统解决的是小型超市日常经营里最头疼的问题——商品种类多了之后库存到底有多少、哪些该补货、一天卖了多少钱、利润大概是多少。如果你是正在做课设或毕设的学生或者刚学完 Java 想找个完整项目练手的人这篇拆解可以帮你省下大量试错时间如果你想给家里的小店配个简单的管理工具这套源码改改也能直接上。1. 项目定位与需求拆解1.1 小型超市管理到底在管什么很多同学拿到题目第一反应是“不就是增删改查吗”这话没毛病但真到答辩的时候老师问一句“你的系统解决了什么业务痛点”不少人就卡住了。所以先说清楚需求。小型超市和大型连锁超市的区别在于没有复杂的供应链系统没有自动补货算法甚至没有专职的 IT 人员。老板最关心的是三件事第一店里现在有什么货、还剩多少第二这批货进价多少、卖价多少、卖出去能赚多少第三一天下来总营业额是多少哪些商品卖得好。围绕这三个问题系统必须包含几个基础功能模块商品管理录入商品名称、编码、条码、规格、单位、进价、售价、库存数量商品分类把饮料、零食、日用品分开方便筛选和统计供应商管理记录从谁那里进货联系电话是多少方便后续补货进货管理采购入库时增加库存同时记录进货批次和成本销售管理收银出单时扣减库存记录每笔销售明细和总金额统计报表按日、按月查看销售额、订单量、利润。别小看这些模块它们之间的数据关系才是重点。比如销售模块不只是往订单表插一条数据它必须同时修改商品库存。如果库存扣减和订单写入没有放在一个事务里就会出现“单子下了但库存没变”或者“库存变了但单子丢了”的情况。这个后面第三节我会展开讲。1.2 技术栈选型为什么是 Spring Boot MyBatis MySQL这套免费源码采用的是 Spring Boot 2.x MyBatis Thymeleaf MySQL 8.xMaven 构建前端页面用 Bootstrap 做样式。这个组合在课程设计和毕业设计里非常常见选它有几个很实际的原因。第一Spring Boot 解决了大量配置问题。以前用 SSM 要写一堆 XML 配置Spring Boot 只需要一个启动类加上 application.yml 就搞定对忙着赶论文的学生来说能把时间花在业务逻辑上而不是花在“配置文件写错导致启动失败”上。第二MyBatis 比 JPA 更直观。JPA 的自动建表和关联查询在简单项目里确实省事但答辩时老师问“你这个 SQL 写在哪”你指着接口说“这是自动生成的”多少有点心虚。MyBatis 的 SQL 自己写、自己控制既能展示你的数据库功底也方便针对报表需求做复杂查询。第三Thymeleaf 服务端渲染避免了前后端分离的跨域问题。如果你用 Vue Spring Boot还得考虑 CORS、Token 鉴权、接口文档对一个超市管理系统来说有点过度设计。Thymeleaf 直接在服务端把数据塞进页面本地直接访问 localhost 就能看到完整效果演示起来非常流畅。当然也有人用 Java Swing MySQL 做桌面版逻辑更简单但界面效果比较老旧而且没办法做多台收银机共用一套数据。用 Web 版至少以后想扩展到“前台收银 后台管理”模式时不用推翻重来。2. 数据库设计先把表结构想清楚2.1 从商品到订单核心表模型拿到源码之后我建议你第一件事不是急着运行而是打开数据库建表脚本看一遍。这套系统的表结构设计得比较干净一共有六张核心表关系也清晰。用户表sys_user存登录账号密码分管理员和收银员两种角色分类表category存商品分类比如饮料、零食、日用供应商表supplier存供货商名称和联系方式商品表product是最核心的表包含商品编码、名称、分类、单位、进价、售价和当前库存销售主表sale记录每一笔销售的订单号、总金额、操作员和时间销售明细表sale_item记录一笔订单里的具体商品、数量和单价。这里最关键的设计是“销售主表 销售明细表”拆开。为什么不直接在订单表里存所有商品因为一次购买可能包含多件商品如果全塞进一张表查订单号的时候就要重复存一堆相同信息数据冗余严重。拆成主表和明细表之后主表存“这一次买了多少钱”明细表存“每一行买了哪个商品”通过 sale_id 关联符合数据库范式也方便以后做“客单价”这类统计。商品表和分类表、供应商表也是外键关系。商品挂在某个分类下分类删了商品就变成无家可归所以删除分类前必须先处理该分类下的商品代码里要写限制逻辑不能光靠数据库约束。2.2 字段设计的坑价格别用 float库存必须有约束建表脚本虽然不起眼但里面有几个字段设计经验值得抄下来。价格字段不要用 float 或 double。浮点数在计算机里是近似存储1.1 2.2 可能算出 3.3000000000000003金额计算用浮点数很容易出现“差一分钱”的诡异问题。这个系统的价格字段用的是 decimal(10,2)十位整数两位小数既满足需求又避免精度丢失。库存字段也不能裸奔。MySQL 里如果不对库存做非负约束应用层一旦扣过头就会出现“库存 -5 件”的奇葩数据。这个案例的解决思路是“应用层判断 数据库兜底”扣减库存的 SQL 写成UPDATE product SET stock stock - #{quantity} WHERE product_id #{id} AND stock #{quantity}利用数据库行锁和条件更新来保证不会扣成负数。这个写法比先查再判的“两步走”安全得多后面并发问题里会再提。时间字段用 datetime 类型默认值设为 CURRENT_TIMESTAMP这样插入订单时不用手动填时间SQL 自动带。还有一个容易被忽视的点逻辑删除。商品如果下架了不要物理删除记录否则历史销售明细里的商品名会变成残缺数据。在这套源码里商品表留了 status 字段0 表示停用1 表示启用查询时默认过滤停用商品。3. 核心功能实现从登录到销售的关键代码3.1 项目分层结构Controller 别写业务逻辑源码的程序包结构是标准的四层controller 控制层、service 业务层、mapper 数据层、entity 实体层。第一次打开项目时你可能会觉得类很多但每个类的职责非常单一。Controller 只干两件事接收前端请求参数调用 Service 层把结果放到 Model 里返回页面。Service 负责写业务逻辑比如“新增商品时检查编码是否重复”“库存不足时不能下单”。Mapper 层用 MyBatis 接口加 XML 文件一条 SQL 对应一个方法。我见过太多课程设计代码是“一个 Controller 走天下”所有逻辑全写在 Controller 里页面上点击按钮直接调 Mapper。这样做当然也能跑起来但答辩时老师稍微加一个需求比如“销售时要考虑会员折扣”你就得在 Controller 里翻来翻去找对应的参数。分层虽然多写几个类的代码但对维护和扩展的收益是立竿见影的。以商品列表页为例Controller 里的代码大概是这样的Controller RequestMapping(/product) public class ProductController { Autowired private ProductService productService; GetMapping(/list) public String list(Model model, RequestParam(defaultValue 1) int page, RequestParam(required false) String keyword) { PageInfoProduct pageInfo productService.findPage(page, 10, keyword); model.addAttribute(pageInfo, pageInfo); return product/list; } }Controller 里不直接写 SQL也不直接 new Service全部靠 Spring 注入测试的时候可以很方便地替换实现。这就是“控制反转”的实际意义——不是面试八股文而是真实项目里的常规操作。3.2 商品管理的分页和模糊查询商品列表几乎是所有管理系统都会有的页面但“分页 模糊查询”的实现方式值得单独拿出来说。这套源码用了 PageHelper 分页插件Service 层不需要手动拼 LIMIT只需要在查询前调用 PageHelper.startPage()后面的查询会自动带上分页参数。Service public class ProductService { Autowired private ProductMapper productMapper; public PageInfoProduct findPage(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListProduct list productMapper.findByKeyword(keyword); return new PageInfo(list); } }Mapper 的 XML 里模糊查询用了 MyBatis 的where和if动态 SQLselect idfindByKeyword resultTypecom.example.entity.Product SELECT * FROM product where if testkeyword ! null and keyword ! name LIKE CONCAT(%, #{keyword}, %) OR code LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY product_id DESC /select这里有个细节要注意#{keyword}和${keyword}的区别。用${}字符串拼接会有 SQL 注入风险比如传入 OR 11整张表的数据都能查出来。用#{}参数绑定MyBatis 会帮你转义安全很多。另外MySQL 里 LIKE 拼接推荐用 CONCAT 而不是直接在 Java 里拼好传进去这样做的好处是防注入也方便后续改成全文索引。3.3 销售入库事务与库存扣减是关键销售模块是整个系统里最容易出错的部分。那套源码里下订单时执行的操作是先生成销售主表记录再遍历购物车里的每个商品把明细插入销售明细表同时每个商品的库存减一。这一步如果中途出错要么全成功要么全回滚不能出现“订单已经生成但库存没减”的中间状态。实现方式其实很简单一个注解就能搞定Transactional(rollbackFor Exception.class) public Sale createSale(SaleRequest request) { Sale sale new Sale(); sale.setSaleNo(generateSaleNo()); sale.setTotalAmount(calculateTotal(request)); saleMapper.insert(sale); for (SaleItem item : request.getItems()) { Product product productMapper.selectByIdForUpdate(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new ServiceException(商品库存不足 product.getName()); } productMapper.decreaseStock(item.getProductId(), item.getQuantity()); item.setSaleId(sale.getId()); saleItemMapper.insert(item); } return sale; }特别注意Transactional默认只对 RuntimeException 回滚。如果你的业务异常是自己定义的 ServiceException而它继承的是 Exception那么默认情况下事务不会回滚数据就写进去了。所以源码里特意写了rollbackFor Exception.class告诉 Spring 只要是 Exception 就回滚。这是很多人容易忽视的点。再看selectByIdForUpdate这个是悲观锁。它的 SQL 是SELECT * FROM product WHERE product_id #{id} FOR UPDATE意思是查出来之后把这行数据锁住直到事务提交或回滚才能释放。为什么必须加这个锁因为如果不锁两个收银员同时卖同一件商品库存还剩 1 件两个人同时查到库存为 1都判断“可以卖”然后都执行减库存最后库存变成 -1。加锁之后第二个事务只能等第一个事务提交后才能查查到库存已经是 0就会提示库存不足。3.4 统计报表用 SQL 聚合代替 Java 循环统计模块最能拉开课程设计分数的差距。大部分人的报表实现方式是查出所有订单然后在 Java 里用 for 循环遍历手动累加每天的销售额。这样做功能上没错数据量小的时候也感觉不出来慢但一旦订单量上万页面加载会明显卡顿。这套源码的做法是直接在 SQL 里聚合一分钟都不到就能完成统计SELECT DATE(sale_time) AS sale_date, COUNT(DISTINCT sale_id) AS order_count, SUM(total_amount) AS turnover FROM sale WHERE sale_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(sale_time) ORDER BY sale_date;这个查询做了三件事按天分组、统计每天订单数、汇总每天营业额。数据库本身是经过高度优化的聚合引擎用 GROUP BY 和 SUM 来处理统计数据远比在 Java 层循环高效。而且 SQL 语句可以直接拿给老师看一眼就能看出你对数据库的掌握程度。如果你要算利润还需要关联商品表的进价字段。这时候可以用 JOIN 把 sale_item 和 product 连起来SUM((sale_item.price - product.cost) * sale_item.quantity)就是毛利。这套系统的报表页面还附了简单的柱状图用的是前端图表库数据还是后端接口返回的 JSON。4. 实操过程中踩过的坑与排查记录4.1 MySQL 8 连接失败时区与驱动版本拿到源码后跑起来最常遇到的第一坑就是数据库连接报错。如果你本地装的是 MySQL 8.x而源码里的配置文件还停留在 MySQL 5.x 的写法启动 Spring Boot 时大概率会报Public Key Retrieval is not allowed或者Could not create connection to database server。解决方案有两个。一个是改数据库驱动版本确认 pom.xml 里用的是com.mysql:mysql-connector-j而不是老旧的mysql:mysql-connector-java另一个是改连接串在 url 后面加上三个参数spring: datasource: url: jdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai是处理时区问题的MySQL 8 默认的美国时区会让 Java 的日期时间和你本地对不上。allowPublicKeyRetrievaltrue是解决 MySQL 8 的密码公钥获取问题。这两个参数不加项目是起不来的。4.2 页面中文乱码从数据库到页面逐层检查中文乱码问题在 Windows 上特别常见。一旦商品名称或者分类显示成乱码你可能会在页面加meta charsetUTF-8但发现没有用。这里给你一个排查顺序。第一层是数据库。建库脚本里要指定字符集CREATE DATABASE supermarket CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci如果已经建好了可以执行ALTER DATABASE supermarket CHARACTER SET utf8mb4。第二层是连接串url 里必须有characterEncodingutf8。第三层是 IDE 的编译编码Spring Boot 启动后如果有中文注解在 pom.xml 里设置一下properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties第四层才是页面。Thymeleaf 默认是 HTML5 编码基本不会乱。乱码大多数情况是第一层或者第三层的问题不需要乱改页面。4.3 库存扣减并发从超卖到乐观锁前面的购物车示例用的是悲观锁 FOR UPDATE适合课程设计场景。但这个方案有个缺点并发高的时候锁冲突大性能不好。如果你想让老师眼前一亮可以在代码里加一个乐观锁方案作为对比。乐观锁的思路是给商品表加一个 version 字段更新库存时带上 version 条件如果 version 变了说明数据被别人改过这次更新失败重新读取再重试。更新 SQL 长这样UPDATE product SET stock stock - #{quantity}, version version 1 WHERE product_id #{id} AND version #{version} AND stock #{quantity};这段 SQL 一条语句同时做了三件事扣减库存、版本号自增、检查库存足够。如果更新的返回行数为 0说明要么库存不足要么 version 不匹配代码里就抛异常回滚。这样既保证了不会超卖又避免长时间锁行。我在实际项目里更常用乐观锁因为超市收银场景的并发量说大不大说小不小乐观锁的体验更流畅。4.4 启动即报找不到 Mapper还有一类问题打开项目编译时提示Invalid bound statement (not found)。通常是两层原因第一Mapper 接口和 XML 文件的 namespace 写错了接口全限定名必须和 namespace 一一对应第二XML 文件没有被 Maven 打包到 classes 目录下因为 MyBatis 的 XML 放在src/main/java里时默认不会被复制到 classpath需要在 pom.xml 里配置资源目录。build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /build这个问题非常隐蔽编译期不报错运行期才报排查起来挺浪费时间。建议养成好习惯Mapper XML 放在src/main/resources/mapper/下并通过mybatis.mapper-locations指定路径避免打包时的各种幺蛾子。5. 源码怎么用以及怎么改造成自己的项目5.1 拿到源码后的第一步先初始化数据库再改配置“白嫖”来的免费源码不管是从哪个渠道下载第一步千万别急着点运行。先把压缩包解压看 README 或者 SQL 目录找到 init.sql 建表脚本在本地 MySQL 里执行把库和表先建出来顺便看一眼有没有内置的账号数据。然后打开src/main/resources/application.yml改三个地方端口默认 8080冲突就改 8081、数据库账号、数据库密码。改完之后启动项目浏览器访问http://localhost:8080用内置账号登录。如果登录成功说明整个链路是通的这时候再去点击页面各种功能把系统当成真正的小商店来操作录入几个商品、做一笔销售、看报表有没有数据。这个流程能让你快速建立“系统全貌”的意识。代码可以一行一行慢慢读但系统必须先跑起来有了画面感后面的代码才能读进去。5.2 把“超市”改成“书店”或“药店”很多人下源码是想交作业但直接原封不动上交容易撞车。我建议你基于这套系统做二次开发把它改成别的业态比如小型书店管理系统、社区药店管理系统、零食店管理系统。改动量不大主要集中在三个地方。第一是实体和数据库字段。书店不需要“保质期”和“供应商”需要“作者”“出版社”“ISBN”把商品表里对应的字段改掉就行。第二是页面标签把 Thymeleaf 页面里的“商品名称”改成“书名”“分类”改成“图书分类”“进价、售价”改成“定价、售价”。第三是统计报表书店更关心“各出版社图书销量占比”那报表 SQL 的 GROUP BY 就要从按分类改成按出版社。这套系统的价值就在它的骨架是通用的商品类目、进销存、统计报表放到任何零售业态都成立。你只需要换皮换肉再加一点行业特有的小逻辑就完全可以说成是自己的设计与实现。5.3 可以走得更远的升级方向如果你有余力这套系统还能往这几个方向升级每一个方向都够写进毕业论文的“未来展望”里。第一个方向是前后端分离。把 Thymeleaf 页面换成 Vue 3 Element Plus后端改为纯 RESTful API登录用 JWT 做无状态鉴权Redis 存登录态。这样技术上更“现代”但工作量也不小。第二个方向是加角色权限管理。现在的用户表只有“管理员”和“收银员”可以升级成 Spring Security RBAC给不同的角色分配不同的菜单权限收银员只能开单不能看成本价店长才能看利润。第三个方向是引入缓存。商品分类这种不常变的数据可以 Redis 缓存报表数据可以异步生成、定时刷新避免每次都重算全量数据。每个方向都足够写 3000 字的设计思路和 1000 字的测试对比数据答辩的时候这就是你的差异化亮点。最后再说个实在话。这套免费源码我建议拿到手之后不要直接跑先花一个晚上把数据库脚本、Controller、Service 三层代码全读一遍再自己手写一遍销售模块的接口。写不出来就对着抄抄完再删直到能闭着眼睛把库存扣减的事务逻辑讲清楚。源码可以白嫖但知识得自己长到脑子里不然答辩的时候老师一个追问就能让你露馅。
返回列表