资讯动态

Spring Boot + 无人超市管理系统设计与实现全解析

发布时间:2026/10/22 3:04:18 来源:尧图企业网站定制
先说结论这套系统的技术选型很典型不算花哨但胜在扎实。如果你正打算做毕设或者课设需要一个既能体现完整业务闭环、又不会在技术上过于冒险的题目Spring Boot 无人超市管理这个方向非常合适。它覆盖了从用户端扫码进店、自助选购、结算支付到管理端商品维护、库存监控、订单管理、数据统计的完整链路适合用来展示你对 Java Web 全栈开发的理解也方便后续扩展人脸识别、物联网设备对接等进阶功能。我自己在带学生做类似项目时见过太多一开始追求高深技术最后烂尾的案例反而是这种“业务清晰、技术主流、边界可控”的选题能稳步做完。这篇文章就以这个题目为例把从需求拆解、数据库设计、核心代码实现到常见坑位的完整思路捋一遍希望能给正在纠结怎么入手的朋友一些可落地的参考。1. 项目整体设计与思路拆解1.1 需求定位无人超市管理系统到底要管什么很多人一看到“无人超市”就想着要搞人脸识别、重力感应货架、RFID标签这些确实是无人零售的方向但如果你是做管理系统核心不在“识别”上而在“管理”上。一个合格的无人超市管理系统首先要回答三个问题谁来卖、卖什么、卖出去之后怎么管。放在系统里就是三个核心角色加一条业务主线顾客端小程序/H5/自助收银台进店、扫码或输入商品编号、加购、结算、支付、查看订单。管理端Web管理后台商品维护、品类管理、库存管理、订单查看、会员管理、数据统计。系统支撑登录认证、权限控制、操作日志。业务主线就是顾客进店 - 选购商品 - 结算支付 - 订单生成 - 库存扣减 - 数据汇总。把这个主线和角色理清楚你再去画用例图、设计数据库就有据可依了不会出现“功能是别人的需求文档抄来的自己却不知道为什么要做这个功能”的情况。1.2 技术选型为什么是Spring Boot MySQL MyBatis-Plus这个项目名字里已经明确写了 Spring Boot 和 Java剩下的技术栈组合需要自己定。我最推荐、也是实测最稳的一套组合是技术层次选型方案理由后端框架Spring Boot 2.x约定优于配置内嵌Tomcat独立启动适合快速开发与部署持久层框架MyBatis-Plus单表CRUD不用写SQL分页插件好用配合条件构造器能省大量代码数据库MySQL 5.7 / 8.0稳定、主流、资料多团队协作和运维都方便权限认证Sa-Token 或 JWT Spring Security轻量用Sa-Token登录态管理简单拦截器配置直观适合中小型项目前端管理端Vue 2 / Element UI 或者直接用 Thymeleaf 模板如果前端基础一般建议直接用模板引擎减少跨域和联调成本接口文档Knife4jSwagger增强版接口调试方便答辩展示效果好缓存可选Redis用于验证码存储、热点商品缓存可作为加分项你可能听说过有人用 Shiro有人用 Spring Security甚至有人直接用原生的 Session 拦截器。我的建议是如果这是课设或毕设不要在权限框架上花太多时间研究源码选一个能快速跑通、又能讲清楚原理的就行。Sa-Token 是一个很好的选择API 简单官方文档中文友好自己看一遍就能上手。1.3 系统架构与目录结构规划这套系统的架构遵循经典的三层结构Controller接口层- Service业务层- Mapper数据访问层。这也是大多数中小型管理系统的主流写法层次清晰、便于讲解和扩展。我规划的标准目录结构大致长这样com.unmanned.supermarket ├── controller // 接口层接收前端请求返回统一响应 │ ├── admin // 管理端接口商品、订单、用户、统计等 │ └── customer // 顾客端接口浏览、购物车、结算等 ├── service // 业务层核心业务逻辑 │ ├── impl ├── mapper // 数据访问层 ├── entity // 数据库实体类 ├── dto // 前端交互的数据对象 ├── vo // 视图返回对象 ├── config // 配置类拦截器、跨域、Knife4j等 ├── common // 公共类统一返回体、异常处理、常量 ├── utils // 工具类JWT、验证码、金额计算等 ├── interceptor // 拦截器登录校验、权限校验 └── SupermarketApplication.java实际开发时建议严格按照这个分层来写。很多同学为了图快把业务逻辑直接写在 Controller 里当时看着爽后面加需求、排查问题、写文档的时候就会非常痛苦。你答辩的时候老师大概率会问你“为什么设计成三层结构”你把这个目录结构一摆再讲清楚“Controller 只做参数接收和响应包装Service 负责业务规则Mapper 负责数据交互”这一问就过了。2. 核心模块解析与数据库设计2.1 核心业务模块拆解把需求翻译成功能模块大致需要拆出下面七个主要模块商品管理商品的增删改查、上下架、按分类筛选、图片上传、价格管理。品类管理商品分类的维护比如饮料、零食、日用品用于前台展示和后台筛选。库存管理库存数量维护、入库操作、库存预警。这里要注意“多表联动”商品删除前要检查是否有关联订单记录订单创建后要扣减库存取消订单要回补库存这是最容易出 bug 的地方。顾客端选购模拟超市购物车的核心流程——查询商品、加入购物车、修改数量、移除商品、清空购物车、结算。订单管理生成订单、支付模拟、订单列表查询、订单详情、取消订单、售后处理。会员管理用户注册登录、会员等级普通会员、黄金会员等、积分累计、积分抵扣。数据统计销售额、订单量、热销商品排行、按日/周/月维度的趋势图。这个模块虽然是简单的 SQL 聚合查询但展示效果最直观建议一定要做答辩时加分明显。2.2 数据库表结构设计要点数据库是整个系统的地基设计得好写代码能少踩很多坑。按照上面的模块至少需要这几张核心表表名说明关键字段user系统用户管理端顾客端可分开或共用id, username, password, phone, role, create_timecategory商品分类id, name, parent_id, sort_orderproduct商品信息id, name, category_id, price, stock, image, status, salescart购物车如果购物车存在后端数据库id, user_id, product_id, quantity, create_timeorder_info订单主表id, order_no, user_id, total_amount, pay_amount, status, create_timeorder_item订单明细表id, order_id, product_id, product_name, price, quantity, subtotalmember_level会员等级可选id, level_name, discount, min_amountstock_log库存变动日志强烈建议加id, product_id, change_type, change_quantity, create_time这里想重点强调两个很容易被忽视的设计细节第一个是“订单主表 订单明细表”的双表结构。很多初学者会把订单设计成一张表把商品直接存成字符串塞进去比如“可乐x2薯片x1总价25元”。这样做的后果是你想统计某个商品的销量时完全没法用 SQL 查。正确做法是订单主表存订单的整体信息单号、用户、总价、状态订单明细表存每一个商品的购买快照商品名、单价、数量、小计。注意这里存的一定是“快照”也就是当时下单时商品的价格和名称而不是去关联 product 表。因为商品名称和价格随时可能改如果两张表通过外键关联历史订单的数据就会被篡改这在业务上是不允许的。第二个是资金字段不要用 double 或 float。这是 Java 开发中特别经典的坑。浮点数在机器内部是二进制表示的计算 0.1 0.2 都可能出现精度误差。金额字段在数据库里建议用decimal(10,2)类型Java 程序里用BigDecimal来处理。MyBatis-Plus 里对这个类型的映射自动支持不需要额外配置但你在做运算或者手动赋值时要注意不要用普通double运算。2.3 订单号生成与状态流转订单号生成是每个电商类项目都躲不开的点无人超市也不例外。最简单粗暴的是用当前时间戳但并发情况下容易撞号。我建议用“时间戳 随机数 用户ID后缀”的组合方式示例代码public static String generateOrderNo(Long userId) { // 格式yyyyMMddHHmmss 3位随机数 用户ID后4位 SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss); String timePart sdf.format(new Date()); int randomPart (int) ((Math.random() * 9 1) * 100); String userPart String.format(%04d, userId % 10000); return timePart randomPart userPart; }订单状态我一般用整数状态机管理0-待支付1-已支付2-已取消3-已完成4-退款中5-已退款。用整数存数据库用常量类定义名称。不要用字符串直接存“未支付”“已支付”这种中文值因为一旦需求方改个叫法你就要全表 UPDATE而用数字常量只改一处定义即可。状态流转要注意“作废/取消”操作的时机。例如待支付的订单用户自己取消或者超时未支付由定时任务自动取消取消后必须回补商品库存。这块逻辑如果不做就会出现“用户下单没付款库存却一直锁着”的问题。3. 实操过程与核心环节实现3.1 环境准备与项目初始化开始之前先确认本地环境JDK 1.8 或 11、Maven 3.6、MySQL 5.7、IDEA。Spring Boot 版本我建议选 2.7.x最后一个支持 JDK8 的稳定版本不要一上来就上 Spring Boot 3因为 3 里面强制要求 JDK17且部分老版本依赖不兼容容易给自己找麻烦。创建项目有两种方式一种是去 Spring Initializr 网站生成基础骨架另一种是直接用 IDEA 自带的 Spring Initializr。我习惯在 Initializr 网页上选好依赖下载下来再用 IDEA 打开顺便指定好 Maven 仓库地址。常用的依赖就四个Spring Web、MyBatis Framework、MySQL Driver、Lombok。其他像 Sa-Token、Knife4j、Redis 之类的依赖直接写在 pom.xml 里引入就行。pom.xml 里几个关键依赖的引入方式!-- MyBatis-Plus 代码生成与基础 CRUD -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency !-- Sa-Token 权限认证框架 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.37.0/version /dependency !-- Knife4j 接口文档 -- dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi2-spring-boot-starter/artifactId version4.4.0/version /dependencyapplication.yml 里的关键配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/unmanned_supermarket?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0两个细节值得注意。第一map-underscore-to-camel-case一定要设为 true这样数据库里的create_time才能自动映射到 Java 属性里的createTime否则你就要在每个实体类上写一堆TableField注解。第二logic-delete-field是 MyBatis-Plus 的全局逻辑删除配置只要实体类里有deleted字段删除操作就自动变成 UPDATE而不是物理 DELETE。逻辑删除的好处是保留历史数据方便追溯和统计。3.2 购物车与自助结算流程的代码实现购物车是无人超市顾客端的核心功能。这里我采用的方案是购物车数据存 Redis结算时同步一份快照到订单明细表。用 Redis 的好处是访问速度快、天然支持过期时间顾客在自助收银台前操作时体验更流畅。当然如果你们实际项目中 Redis 用的不够熟练也可以直接存在数据库表里用user_id作为用户购物车标识效果差距不大关键是逻辑要跑通。购物车操作的核心接口RestController RequestMapping(/api/cart) public class CartController { PostMapping(/add) public Result addToCart(RequestBody CartItemDTO dto) { // 1.校验商品是否存在以及是否上架 Product product productService.getById(dto.getProductId()); if (product null || product.getStatus() ! 1) { return Result.fail(商品不存在或已下架); } // 2.校验库存是否充足 if (product.getStock() dto.getQuantity()) { return Result.fail(库存不足); } // 3.写入Redis购物车key为 cart:userId 使用Hash结构 cartService.addToCart(getLoginUserId(), dto); return Result.success(); } GetMapping(/list) public Result cartList() { ListCartItemVO list cartService.getCartList(getLoginUserId()); return Result.success(list); } DeleteMapping(/remove/{productId}) public Result removeCartItem(PathVariable Long productId) { cartService.removeFromCart(getLoginUserId(), productId); return Result.success(); } }这里的getLoginUserId()是通过 Sa-Token 拦截器里已经写入当前登录用户上下文的方法获取的不用每次从 token 里手动解析。结算与下单的核心代码Transactional(rollbackFor Exception.class) public OrderVO checkout(Long userId) { // 1.获取用户购物车列表 ListCartItem cartItems cartService.getCartList(userId); if (cartItems.isEmpty()) { throw new BizException(购物车为空无法下单); } // 2.计算总金额校验库存采用悲观锁或乐观锁机制避免超卖 BigDecimal totalAmount new BigDecimal(0); for (CartItem item : cartItems) { Product product productService.getByIdWithLock(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BizException(商品「 product.getName() 」库存不足); } BigDecimal subtotal product.getPrice().multiply(new BigDecimal(item.getQuantity())); totalAmount totalAmount.add(subtotal); } // 3.生成订单主表记录 OrderInfo order new OrderInfo(); order.setOrderNo(generateOrderNo(userId)); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setPayAmount(totalAmount); order.setStatus(0); // 待支付 orderInfoMapper.insert(order); // 4.生成订单明细快照注意保存当时的商品名称和价格 // 5.执行库存扣减这里用 UPDATE ... SET stock stock - #{quantity} 的原子写 // 6.清空购物车 cartService.clearCart(userId); return OrderVO.from(order); }这里有一个非常关键的细节扣减库存不能先查后减否则在并发场景下会出现超卖。正确做法是直接用 SQL 语句做原子扣减Update(UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}) int decrStock(Param(productId) Long productId, Param(quantity) Integer quantity);如果返回的影响行数为 0说明库存已经不够了这时候再抛异常提示用户。这个写法是解决高并发超卖问题的经典方案面试官问起并发问题时你也能主动说出这个方案。3.3 数据看板与销售统计数据看板是很多同学喜欢最后才做甚至干脆不做的模块。但实际上它是最能体现“系统价值”的功能毕竟无人超市运营方最关心的就是卖了多少、赚了多少、什么东西好卖。数据统计模块的核心就是几条 SQL今日销售额SELECT IFNULL(SUM(pay_amount), 0) AS amount FROM order_info WHERE status 1 AND DATE(create_time) CURDATE()热销商品 TOP 10SELECT product_name, SUM(quantity) AS total_sales FROM order_item GROUP BY product_id ORDER BY total_sales DESC LIMIT 10近7日订单趋势SELECT DATE(create_time) AS day, COUNT(*) AS order_count, SUM(pay_amount) AS amount FROM order_info WHERE status 1 AND create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day如果在答辩时想加分可以借助 ECharts 把这些数据渲染成折线图和柱状图。后端返回 JSON 数组前端直接绑定数据到图表实例即可效果非常直观。4. 常见问题与排查技巧实录这套系统写下来绝大多数人会踩的坑基本就在下面这几个地方。我把它们整理出来并给出了排查思路和解决方案希望你能提前避开而不是周末熬夜查 bug。4.1 事务不生效库存扣减异常这是我在批改代码时见最多的问题。Transactional注解默认只能拦截 RuntimeException 异常而且必须是自调用之外的方法调用。如果你在同一个类里用this.checkout()调用自身事务是不会生效的如果你在方法里用 try-catch 把异常吞掉了事务也不会回滚。解决方式有两种把事务方法放到另一个 Service 中通过 Spring 容器注入调用例如在OrderServiceImpl里调用CartServiceImpl的回滚方法。在Transactional里显式指定回滚类型Transactional(rollbackFor Exception.class)并且不要让异常被调用方吞掉。不管哪种方式都要清楚一点事务要生效前提是这个调用链必须经过 Spring 代理。自己调用自己代理就失效了。4.2 前后端联调时的跨域问题如果你用的前端是 Vue开发时前端在 8081 端口后端在 8080 端口那么你调接口一定会遇到跨域问题。解决方式是在后端做一个全局 CORS 配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }如果配置了 Sa-Token 的拦截器需要先放行预检请求OPTIONS否则同样会跨域失败。这里注意 Sa-Token 的注册顺序要在 CORS 配置之后。4.3 数据库时间与本地时间相差 8 小时这是又一个经典问题。原因是数据库连接的serverTimezone没有设置成Asia/Shanghai导致 JDBC 驱动用 UTC 时间解析。解决方式有两个一是在连接串里加上serverTimezoneAsia/Shanghai二是在 Spring Boot 中配置spring.jackson.time-zoneGMT8。如果你实体类里的时间字段用了LocalDateTime建议搭配JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解来统一格式化。4.4 图片上传后前端显示不出来商品模块通常要支持上传图片很多同学用相对路径把图片存在本地目录结果前端访问时 404。建议的做法是在配置文件中自定义一个上传路径映射Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file: uploadDir /); } }这样前端回显时直接用/images/xxx.jpg访问即可。不要用文件的真实磁盘路径拼到页面上那是绝对路径部署到服务器上必挂。5. 性能优化与后续扩展思路5.1 大批量查询时的分页与缓存商品列表和订单列表在数据量上来之后如果不做分页数据库压力会非常大。MyBatis-Plus 的Page对象用法很简单PageProduct page new Page(current, size); QueryWrapperProduct wrapper new QueryWrapper(); wrapper.eq(status, 1) .like(StringUtils.hasText(keyword), name, keyword) .orderByDesc(create_time); productService.page(page, wrapper);这里注意like的第一个参数是 boolean 类型判断条件只有关键词不为空时才拼接这个条件能有效避免空值导致 SQL 拼接问题。热点商品可以用 Redis 做缓存每天定时刷新一次缓存数据。顾客端首次查询时查库后续直接走缓存响应时间能从几百毫秒降到几十毫秒。但要注意一个问题如果商品修改了价格缓存里的旧数据会继续存活需要设置合理的过期时间比如 30 分钟或者在后台修改商品时主动删除对应缓存 key。5.2 数据统计怎么应对客户临时加需求“临时加需求”是项目开发里永远躲不开的事情无人超市也不例外。我在线上遇到最多的是两类需求一是“老板想看每个分类的销售占比”二是“想看回头客的消费频次”。应对这类需求我的建议是不要直接写死 SQL 后硬编到代码里而是把数据看板模块做成“通用查询接口”。你前期设计订单明细表时如果把category_id和category_name冗余进去那么后面所有统计都只需要GROUP BY category_id不用去关联分类表。这就是数据库设计里常说的“查询场景的冗余设计”牺牲一点存储空间换来的是查询逻辑极简和灵活性提升。5.3 后续还能往哪些方向扩展如果做完基础功能想锦上添花可以考虑这几个方向引入 Redis 验证码用户注册、登录增加图形验证码或短信验证码短信需要第三方服务。对接支付模拟接口本地实现一个模拟支付网关支持余额、积分、优惠券抵扣演示效果很好。增加库存预警定时任务每 10 分钟扫描一次库存低于阈值的商品通过站内信或邮件提醒运营人员。多层会员体系与营销活动满减、折扣、限时秒杀。这些也是很好的面试素材尤其是限时秒杀可以延伸到 Redis Lua 或消息队列的话题。不过我也要提醒一句扩展功能永远是在基础功能完全跑通、文档写好的前提下才做。很多人贪多嚼不烂核心流程没做完就急着加秒杀、加分布式锁最后主线功能一塌糊涂答辩时连订单状态流转都讲不清楚得不偿失。6. 一些做这类项目的总结性建议根据我个人的实操经验最后给你几条具体建议。第一建表脚本一定要先在本地跑通。很多人不重视这一步代码写到一半才发现某个字段漏了再回头改实体类、改 SQL、改前端页面改到怀疑人生。我习惯的做法是先建库建表填入少量测试数据再用 Navicat 或 DataGrip 把表结构截图保存到项目 docs 目录这样写文档和答辩展示都很方便。第二接口设计从一开始就用统一的返回格式。我常用的结构是{ code: 200, message: success, data: {...} }写一个 Result 工具类所有接口都返回这个结构不要一会返回 Map 一会返回实体类否则前端联调时会非常痛苦。如果你后端代码比较多也可以用RestControllerAdvice做全局异常拦截统一封装异常信息。第三写代码前先画清楚状态流转图。订单状态、库存状态、支付状态这些都涉及多步骤流转建议先用一张表格整理清楚“谁在什么条件下能触发什么操作”然后再去写代码。很多逻辑 bug 都是因为状态没理清楚比如“已支付”的订单能不能被用户删除“已完成”的订单能不能申请售后这些规则你作为开发者必须先定好不能等测试同学来提。第四不要忽视单元测试。无需全模块覆盖但核心的服务方法下单、库存扣减、金额计算至少要写几个测试用例。我见过太多人把项目代码写完发现下单后库存没变最后定位到是 SQL 的问题。有了测试这类低级错误一跑就现形不用等部署上去才发现。这套系统我先后在不同场景下搭过好几轮每次都会发现新的细节问题但整体骨架已经验证得非常稳定。希望这篇文章能帮你少走一些弯路也期待看到你完成自己的版本。如果你在实现过程中有具体问题欢迎带着报错信息和相关代码来交流一起把项目打磨得更完善。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑