SpringBoot+MyBatis-Plus构建超市外卖系统:分层架构与订单状态机实战

发布时间:2026/9/11 2:02:58
SpringBoot+MyBatis-Plus构建超市外卖系统:分层架构与订单状态机实战 简介面向Java方向毕业设计与课程作业场景基于Spring Boot的超市外卖系统资料完整覆盖用户管理、商品管理、购物车和订单管理四类核心业务。用户模块包含注册登录、密码找回与管理员/顾客/配送员等多角色权限商品模块提供分类展示、关键词搜索、价格筛选以及详情页的库存和评价信息购物车支持添加删除、数量调整并实时更新总价下单流程整合配送地址、在线支付、货到付款等环节业务闭环完整适合作为课程设计或毕业设计项目底稿。压缩包共776个文件约23.95MB以210个Java服务端源码、144个Vue前端页面及SVG/PNG图标素材为主同时附带SQL建表脚本、BAT启动脚本、Maven配置等辅助文件目录结构清晰便于定位后端接口、前端组件和数据库初始化内容。额外配有毕业论文与答辩PPT可辅助梳理系统架构、数据库设计及核心流程显著降低从零搭建和文档撰写的起步成本。目前已有51人浏览学习对准备外卖类Web工程或Spring Boot实践项目的读者具有参考价值。1. 拆解超市外卖系统模块边界、角色权限与SpringBoot落地思路“超市外卖”不是一个新商业模式却最适合拿来练SpringBoot——领域足够熟悉订单和库存又恰好卡在“简单到能讲清楚、复杂到能展示水平”的位置。这个基于SpringBoot的毕设项目覆盖注册登录、商品分类检索、购物车、下单支付四个主流程还区分了顾客、管理员、配送员三类角色。用户端按“浏览-加购-下单-支付”推进管理员维护商品与库存配送员处理配送列表。后端用SpringBoot提供REST接口MyBatis-Plus简化数据访问前端构建产物托管为静态资源整个项目靠批处理脚本一键打包启动。适合三类人正在做Java课程作业的学生、需要交毕业设计并获得答辩PPT的本科生、想快速跑通参考实现的开发者。后面按“后端骨架→核心业务→构建部署→验证排错”逐一拆。2. SpringBoot后端分层用户注册、角色权限与商品检索实现2.1 为什么先定分层再写接口很多毕设失败不是因为功能难而是因为所有逻辑堆在Controller里。拿这个项目来说用户注册要做密码加密商品搜索要做条件拼接一旦混在一起后面的订单和购物车代码根本没法维护。常见结构是controller-service-mapper三层Controller只接收参数和返回结果Service放业务规则Mapper面对数据库。对这个项目我建议再加上dto和entity的区分entity和表结构一一对应dto用于接口入参和出参避免把密码hash直接暴露给前端。拿这个项目里最容易写脏的“注册”来说Controller里的代码只需十几行。Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public Result register(RegisterDTO dto) { // 1. 参数校验用户名、手机号不能为空 if (!StringUtils.hasText(dto.getUsername()) || !StringUtils.hasText(dto.getPhone())) { return Result.error(用户名和手机号不能为空); } // 2. 校验手机号是否已注册 Long count userMapper.selectCount( new LambdaQueryWrapperUser().eq(User::getPhone, dto.getPhone())); if (count 0) { return Result.error(该手机号已注册); } // 3. 密码加密后落库不能存明文 User user new User(); user.setUsername(dto.getUsername()); user.setPhone(dto.getPhone()); user.setPassword(new BCryptPasswordEncoder().encode(dto.getPassword())); user.setRole(RoleEnum.CUSTOMER.getCode()); user.setStatus(1); userMapper.insert(user); return Result.success(); } }说明step 1用于基础非空校验step 2用LambdaQueryWrapper的selectCount防止重复注册step 3用BCryptPasswordEncoder加密而不是自行拼MD5。注意这里没有用Valid注解是因为很多毕设后端没有引入spring-boot-starter-validation如果引入了可以把非空校验移到DTO上。密码找回功能本质上是“先验证手机号再更新password字段为新的密文”复用的就是这三步里的加密逻辑。2.2 角色权限一张表还是三张表这个项目区分普通顾客、管理员、配送员。如果你在答辩时被问“为什么不做Spring Security”标准回答是人员量级小用拦截器注解做角色校验足够Spring Security能做的登录、授权这里也能实现而且更容易讲清楚。角色不算多我建议不要建user、role、user_role三张表直接在用户表加role字段即可。三张表适合权限点很多的后台管理系统超市外卖的核心链路是商品和订单角色只用来限制接口访问范围一张表反而好查。用户表user字段类型说明idbigint主键自增usernamevarchar(50)用户名phonevarchar(20)登录手机号passwordvarchar(100)BCrypt加密后的密码roletinyint1顾客 2管理员 3配送员statustinyint1正常 0禁用角色权限对照角色可用接口典型操作顾客/api/user/、/api/cart/、/api/order/**浏览商品、加购、下单管理员/api/admin/**商品上下架、订单状态修改配送员/api/delivery/**查看待配送列表、标记送达拦截器里做法是从request中取出token或session里的userId查user表拿到role判断该路径是否在允许列表里。管理员的路径以/api/admin开头配送员以/api/delivery开头顾客路径直接放行。改密码接口单独放在/api/user/password下便于统一校验旧密码。2.3 商品检索like查询和分页别写进mapper.xml商品搜索支持关键词、价格区间、分类筛选。很多人一上来就写mapper.xml的动态SQL一大片。这个场景用MyBatis-Plus的LambdaQueryWrapper就够了Java代码里拼条件比XML更直观debug也容易。public PageProduct searchProduct(ProductQueryDTO query) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); // 关键词匹配商品名称 if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Product::getName, query.getKeyword()); } // 分类筛选 if (query.getCategoryId() ! null) { wrapper.eq(Product::getCategoryId, query.getCategoryId()); } // 价格区间 if (query.getMinPrice() ! null) { wrapper.ge(Product::getPrice, query.getMinPrice()); } if (query.getMaxPrice() ! null) { wrapper.le(Product::getPrice, query.getMaxPrice()); } // 上架状态保证只看到可售商品 wrapper.eq(Product::getStatus, 1); wrapper.orderByDesc(Product::getCreateTime); return productMapper.selectPage( new Page(query.getPageNum(), query.getPageSize()), wrapper); }参数说明pageNum和pageSize默认值设为1和10like拼接的是%keyword%数据量过万时性能会下降排序使用orderByDesc按创建时间倒序。如果MySQL数据量变大经验做法是改成前缀匹配价格筛选走索引这些在答辩时可以提一嘴。product表包含id、name、category_id、price、stock、status、main_image、description、create_time。商品详情页展示的就是这些字段库存状态的实时性不必做到秒级秒杀场景才需要Redis预扣普通外卖场景查表就够。3. 购物车与订单状态机从加购到支付的事务设计与Redis改进3.1 购物车数据库临时表还是Redis购物车的实现有两种。第一种是纯前端localStorage只保存商品id和数量下单时一次性提交第二种是后端cart表与用户绑定跨设备同步。这个项目选择了后者原因是订单表需要展示“从购物车继承来的商品快照”如果购物车不落库用户清缓存会把下单数据一起清掉。我把cart表和cart_item合并成一张表核心字段id、user_id、product_id、quantity、checked、create_time、update_time。加购时的逻辑是已存在则数量加一不存在则插入。public Result addToCart(Long userId, Long productId, Integer quantity) { // 1. 商品必须存在且上架 Product product productMapper.selectById(productId); if (product null || product.getStatus() ! 1) { return Result.error(商品不存在或已下架); } // 2. 查询当前用户购物车中是否已有该商品 CartItem exist cartMapper.selectOne(new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, userId) .eq(CartItem::getProductId, productId)); if (exist ! null) { exist.setQuantity(exist.getQuantity() quantity); cartMapper.updateById(exist); } else { CartItem item new CartItem(); item.setUserId(userId); item.setProductId(productId); item.setQuantity(quantity); item.setChecked(true); cartMapper.insert(item); } // 3. 重新计算购物车总价返回给前端 return Result.success(calcCartTotal(userId)); } private BigDecimal calcCartTotal(Long userId) { ListCartItem items cartMapper.selectList(new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, userId)); return items.stream() .map(item - productMapper.selectById(item.getProductId()).getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); }这里有两个容易踩的坑一是商品删除后cart里还存着product_idcalcCartTotal会空指针建议查product前先判断二是如果将来并发高quantity的加减要放在Redis里用incr操作RedisTemplate的opsForValue().increment()如果报“not integer or out of range”多半是value类型不是数字先确认序列化器是不是默认的JdkSerializationRedisSerializer。3.2 订单状态机从待支付到已完成的流转订单模块是这个项目答辩时的亮点。状态机不复杂但一定要统一管理不能散落在if-else里。定义枚举public enum OrderStatus { UNPAID(0, 待支付), PAID(1, 已支付), DELIVERING(2, 配送中), FINISHED(3, 已完成), CANCELED(4, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }流转规则用表记录更直观当前状态触发动作目标状态待支付用户支付已支付已支付商家确认发货配送中配送中配送员标记送达已完成待支付用户取消/超时已取消已支付管理员取消已取消下单接口的Service里事务的边界要画清楚。一个典型的提交流程是校验库存→扣减库存→生成订单主记录→生成订单明细→清空购物车中已选商品。Transactional(rollbackFor Exception.class) public Result createOrder(CreateOrderDTO dto) { // 1. 查询购物车中已勾选的商品 ListCartItem checkedItems cartMapper.selectList(new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, dto.getUserId()) .eq(CartItem::getChecked, true)); if (checkedItems.isEmpty()) { return Result.error(请先勾选要结算的商品); } // 2. 生成订单主记录 Order order new Order(); order.setUserId(dto.getUserId()); order.setAddress(dto.getAddress()); order.setStatus(OrderStatus.UNPAID.getCode()); order.setOrderNo(generateOrderNo()); orderMapper.insert(order); // 3. 写订单明细并扣库存 BigDecimal total BigDecimal.ZERO; for (CartItem item : checkedItems) { Product product productMapper.selectById(item.getProductId()); // 扣减库存更新时带上stock条件防止超卖 int rows productMapper.deductStock(product.getId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(商品[ product.getName() ]库存不足); } OrderItem detail new OrderItem(); detail.setOrderId(order.getId()); detail.setProductId(product.getId()); detail.setProductName(product.getName()); detail.setPrice(product.getPrice()); detail.setQuantity(item.getQuantity()); orderItemMapper.insert(detail); total total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } order.setTotalAmount(total); orderMapper.updateById(order); // 4. 清空已勾选的购物车项 cartMapper.delete(new LambdaQueryWrapperCartItem() .eq(CartItem::getUserId, dto.getUserId()) .eq(CartItem::getChecked, true)); return Result.success(order.getId()); }扣库存这一段是核心。deductStock对应SQL为UPDATE product SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}返回0表示库存不够事务整体回滚这样在高并发时也不会出现负库存。如果只先select再update并发下一定超卖这是面试里最常追问的点。3.3 支付方式在线支付和货到付款怎么设计这个项目支持在线支付和货到付款两种。在线支付显然不会接真实支付宝通常的做法是支付方式实现方式在线支付生成支付单调用一个模拟支付接口修改订单状态为已支付货到付款下单后状态直接置为已支付送达时收钱我的建议是货到付款在下单时就标记为“已支付”。因为支付状态的意义是“钱有没有到位”货到付款钱在配送员手里对系统而言不产生支付接口调用直接进入配送流程更简单。模拟支付的Service里一个方法就够了public Result pay(Long orderId, String payType) { Order order orderMapper.selectById(orderId); if (order null || order.getStatus() ! OrderStatus.UNPAID.getCode()) { return Result.error(订单不存在或状态不正确); } if (ONLINE.equals(payType)) { // 模拟第三方支付回调 order.setStatus(OrderStatus.PAID.getCode()); order.setPayTime(LocalDateTime.now()); orderMapper.updateById(order); return Result.success(支付成功); } if (CASH.equals(payType)) { // 货到付款直接进入配送 order.setStatus(OrderStatus.PAID.getCode()); orderMapper.updateById(order); return Result.success(货到付款确认); } return Result.error(不支持的支付方式); }注意payTime只在在线支付成功时写入货到付款不需要便于答辩时说明这两种方式的状态差异。4. Maven一键构建mvnw、build.bat与application.yml部署配置4.1 Maven Wrapper为什么项目里会有mvnw.cmd打开项目根目录通常会有mvnw.cmd、build.bat、run.bat、install.bat这些文件。mvnw.cmd是Maven Wrapper的Windows脚本它存在的意义是固定Maven版本。你的电脑装了3.8同学电脑可能装3.5同一个pom.xml在不同版本下解析结果不同有了wrapper之后双击mvnw.cmd会用项目指定的Maven版本构建。其实不一定要用wrapper但建议pom.xml里这三个依赖必须有spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-j。MySQL驱动版本要与本机MySQL对应如果你装的是MySQL 8.x驱动用com.mysql.cj.jdbc.Driver。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependenciesSpringBoot版本别追太高。2.7.18是2.x里比较可靠的版本JDK 8也能直接跑。如果你新机器装了JDK 17又非要选SpringBoot 3.xMyBatis-Plus和很多老的starter都要换版本毕设里不值得为这个折腾。4.2 build.bat和run.bat一键脚本到底做了什么项目正文里有build.bat、run.bat、install.bat这套脚本对应的是“安装依赖→打包→运行”三步。很多同学直接双击install.bat发现失败原因是没有先启动MySQL。脚本本身不复杂常见写法如下echo off rem 1. 设置JDK路径 set JAVA_HOMEC:\Program Files\Java\jdk1.8.0_202 set PATH%JAVA_HOME%\bin;%PATH% rem 2. 编译跳过测试 call mvnw.cmd clean package -DskipTests rem 3. 运行打包好的jar java -jar target\supermarket-0.0.1-SNAPSHOT.jar pause对应的打包命令参数可以分开记命令作用mvnw.cmd clean清理target目录mvnw.cmd package打包成可执行jar-DskipTests跳过测试用例不加也能过-Dmaven.test.skiptrue跳过测试编译比skipTests更彻底java -jar启动内置Tomcat有些同学问为什么要打成jar而不是war。SpringBoot内嵌Tomcatjar用java -jar直接启动war要额外配外部Tomcat毕设答辩演示时jar的方式省去部署环节。install.bat做的事基本是mvnw.cmd install把依赖装进本地仓库第一次运行慢是因为要下载依赖后面再跑都是增量。4.3 application.yml数据库、端口和MyBatis-Plus配置启动前把application.yml检查一遍。这份配置是整个项目能不能跑起来的关键server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 servlet: multipart: max-file-size: 10MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto几个容易出问题的点参数常见错误正确做法serverTimezone不设会报时间区错误写Asia/Shanghai不要写UTCuseSSL本地连不上显式falsemap-underscore-to-camel-casecreate_time映射不上必须true否则实体字段全是nullpasswordroot密码不一致改成你本机MySQL真实密码log-impl建议本地开发时打开控制台会打印每条SQL前端说“接口返回不对”时先看SQL执行结果是不是预期。上线前再把log-impl删掉或改成error级别。如果项目里还带了前端构建产物比如main.js.bak、app.dd27da19.css这类文件说明前端是构建过后把静态文件放在resources/static下SpringBoot会直接托管。main.js.bak是备份文件不影响运行删不删都行。5. 验证清单与上线前要填的四个坑5.1 答辩前的功能验证清单按下面表格走一遍能覆盖展示时80%的操作路径。用真实数据而不是默认数据效果更好。模块验证操作预期结果用户注册新手机号注册提示成功数据库出现记录且密码为bcrypt密文用户登录错误密码提示密码错误不返回堆栈商品搜索输入“可乐”只返回名称含可乐的商品购物车加购同一商品两次数量变为2总价自动翻倍下单选择地址提交库存减少购物车已勾选商品清空支付模拟在线支付订单从待支付变已支付管理员下架商品用户端列表不再显示该商品5.2 四个高频坑第一个坑Application启动类扫描不到Mapper。解决方法是在启动类加MapperScan(com.example.supermarket.mapper)或者每个Mapper接口上加Mapper二选一别两个都写。第二个坑前端跨域。如果前端项目单独跑在8081端口后端8080两者端口不同就会遇到CORS。后端加一个CorsFilter或者CrossOrigin最常见的配置是允许所有来源、所有方法。第三个坑首次以管理员身份登录时用户表里没有管理员账号。初始化数据脚本里如果没写就手动在数据库插入一条role2的记录密码用BCrypt生成注意不要直接insert明文密码。第四个坑配送地址字段的类型。配送地址在表单里是字符串但提交到后端时如果用了RequestBody接收DTO字段名和前端JSON字段名不一致会直接反序列化失败。调试时先确认前端传的JSON字段名和后端DTO一致。最后留一个可以马上上手的技巧把库存扣减的update语句用IDEA的SQL日志检查一遍如果发现update影响行数一直是0先查WHERE条件里stock quantity是不是写反了。这一条不解决下单功能在演示现场一定翻车。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询