SpringBoot+Vue网上点餐系统实战:从需求拆解到部署避坑全记录

发布时间:2026/9/17 3:50:59
SpringBoot+Vue网上点餐系统实战:从需求拆解到部署避坑全记录 网上点餐系统这个题目在Java毕设和课设里算是常客了。但说实话十份作品里能称得上“完整可用”的我见过的不超过三份。大多数不是卡在CRUD写不完就是前后端联调直接摆烂最后搭个半成品上去答辩。这次我基于SpringBootVueMySQLMyBatis这套组合完整走了一遍从需求拆解、数据库设计、后端接口实现到前端页面联调的全流程并且把其中真正折磨人的细节——比如MyBatis动态SQL里的类型比较坑、订单事务边界怎么划、Vue打包后资源404怎么处理——全部记录下来。这篇文章不是教科书式的架构讲解就是我实际做这个项目时的完整记录和踩坑复盘适合正在做毕设、课设或者想自己从零写一套前后端分离项目练手的同学参考。1. 先别急着写代码点餐系统的需求拆解和模块边界很多同学拿到这个题目第一反应是建个SpringBoot工程然后把用户表、菜品表、订单表一建就开始写增删改查。这样做到一半一定会乱。原因很简单点餐系统看起来是标准的CRUD但真正写完你会发现角色权限、订单状态流转、库存扣减这些逻辑互相牵扯没有提前把边界划清楚代码写到最后就是一团浆糊。1.1 这个系统到底要解决什么问题网上点餐系统的核心场景是用户打开网页浏览菜品分类把想吃的菜加入购物车提交订单商家在后台看到新订单接单并开始制作用户能看到订单状态变化。贯穿全程的三个核心实体就是菜品、订单、用户。但“能点餐”只是表面需求。真正的需求是“订单状态的完整流转”。用户下单之后订单不是一步到位的它至少经历待支付、待接单、制作中、已完成或者配送中、已取消这几个状态。谁来改状态、怎么保证状态不被乱改、用户取消订单时商家那边怎么同步这些才是系统的骨头。我把角色拆成了三类普通用户、商家管理员、超级管理员。用户负责浏览、下单、支付模拟、取消订单商家负责菜品上下架、分类管理、接单和完成订单超级管理员管用户、管商家、看整体统计数据。三个角色的功能域在模块设计阶段就必须分开不然后端接口权限无从谈起。1.2 角色划分与功能闭环用户端的功能列表看起来很常规注册登录、菜品浏览、按分类筛选、搜索、加入购物车、提交订单、查看我的订单、取消订单。但注意购物车这里就有一个设计分歧——购物车数据存在前端还是后端。我的做法是购物车放在后端采用Redis或者MySQL表存储都行但考虑到这套系统主要用MySQL我直接建了一张cart表。原因是购物车如果只放前端localStorage用户换设备、清缓存购物车就没了而且后端保存购物车下单时可以直接把购物车数据转成订单明细不用前端传一堆参数接口会干净很多。商家端的核心是菜品管理和订单处理。菜品管理里面要注意一个“上下架”逻辑——下架的菜品用户端就看不到了但正在进行的订单不能受影响所以菜品表里要有status字段而不是删记录。订单处理就是修改订单状态待接单改成制作中制作中改成已完成。管理员端就是标准的用户管理和数据统计。用户管理包含封禁/启用账号数据统计可以简单点按日统计订单数、销售额或者统计销量Top10的菜品这些SQL都不复杂但做出来展示效果很好。1.3 非功能需求并发、事务和数据一致性这个项目并发量不会很大不需要上MQ和Redis集群那一套但有两个点必须想清楚下单过程的原子性以及库存扣减的正确性。下单这个动作涉及的操作不止一条SQL要查菜品、计算总价、插入订单主表、插入订单明细表、扣减库存。任何一个环节失败订单都不能残留半截数据所以必须包在一个事务里。库存扣减的SQL不能是先select再update那会出现超卖虽然小项目不容易并发压测出来但写代码的时候就要用update dish set stock stock - 1 where id ? and stock 0这种带条件的更新。这些需求理清楚之后代码才能写得有条理。下面说技术选型。2. 技术选型背后的逻辑为什么是SpringBootVueMySQLMyBatis这个组合是现在Java全栈项目里最主流的搭配但不是因为它“流行”就选它而是每一样都有它不可替代的理由。我逐个说。2.1 后端选型SpringBoot怎么把开发成本压下来SpringBoot最大的价值不是“自动配置”这个噱头而是它把大量原本需要显式配置的东西变成了约定。做这个项目我不需要写一堆XML配置文件不需要手动配置DispatcherServlet一个spring-boot-starter-web依赖加上SpringBootApplication注解一个能跑起来的Web服务就出来了。这里我重点提醒一下版本问题。现在网上很多教程还在用SpringBoot 2.x但直接新建项目很可能会拉到3.x的版本。SpringBoot 3.x要求JDK17起步而且包名从javax.servlet换成了jakarta.servlet这意味着很多老代码复制过来直接编译报错。如果电脑上装的是JDK8老老实实用SpringBoot 2.7.x别追求新版本否则光环境问题就能折腾一整天。我用的就是SpringBoot 2.7.18稳定且网上资料最多。2.2 持久层选型MyBatis的灵活性和可控性MyBatis和Spring Data JPA争论了很久但点餐系统这种SQL逻辑比较灵活的场景我选MyBatis。原因很直接JPA虽然有自动建表、方法名派生查询这些便利但一旦涉及多表关联、动态条件查询要么写JPQL要么写原生SQL反而不如MyBatis的XML直观。MyBatis的核心优势是动态SQL。比如菜品列表的查询条件可能是“分类ID 关键词 上下架状态”的任意组合用where加if标签就能拼出正确的SQL而且肉眼可见、可控性极强。再加上MyBatis的collection标签能方便地映射订单和订单明细这种一对多结构比JPA懒加载踩坑要省心。2.3 前端选型Vue怎么支撑点餐这种交互密集场景Vue的核心价值是组件化和响应式。点餐页面的核心交互——菜品列表、分类切换、购物车角标、购物车抽屉——这些天然适合拆成组件。Vuex管理购物车状态、Vue Router管理页面跳转、Axios统一处理接口请求这三件套一套走下来页面逻辑就清晰了。版本上我用Vue2。不是因为Vue3不好而是Element UI这个组件库和Vue2的配合最成熟网上案例也最多。Vue3配Element Plus当然可以但如果你是想快速把项目做完Vue2的学习成本更低踩坑更容易找到解决方案。2.4 MySQL为什么够用数据量和并发量评估MySQL在这个场景下完全够用不需要考虑分库分表。点餐系统的数据量级撑死几万条菜品、几十万条订单MySQL单表轻松扛住。真正要注意的是表的字段类型和索引订单号的唯一索引、用户ID的普通索引、菜品分类的索引这些加好了查询不会慢。版本我推荐MySQL 5.7或者8.0都行。8.0的窗口函数、JSON字段更好用但驱动配置有点区别——8.0的JDBC驱动是com.mysql.cj.jdbc.Driver而且URL上要加serverTimezoneAsia/Shanghai否则会报时区错误。5.7则是万金油兼容性最好。我都试过没有本质区别选哪个取决于电脑上装了什么。3. 数据库设计点餐系统的表结构该怎么划数据库设计是整个项目的底盘表结构没设计好后面写SQL就是地狱模式。我按“用户体系-菜品体系-订单体系”三条线来设计总共六张核心表。3.1 六张核心表的结构和数据关系用户表sys_user字段id、username、passwordMD5或BCrypt加密、nickname、avatar、roleUSER/ADMIN/SUPERADMIN的整型枚举、status1启用/0禁用、create_time。角色用整型字段存不要用字符串查询和判断都方便。菜品分类表categoryid、name、sort、create_time。分类表只有一层不做无限级分类点餐系统用不上的复杂度就是多余的。菜品表dishid、category_id、name、image、description、price、stock、status1上架/0下架、create_time、update_time。注意price用DECIMAL(10,2)status必须有下架不等于删除。购物车表cartid、user_id、dish_id、dish_name冗余字段、dish_image冗余、price下单时的快照价格、quantity、create_time。这里我冗余了菜品名称和图片是为了查购物车的时候不用关联菜品表性能更好代价是菜品改名后购物车里的名字不同步——但购物车本来就是临时数据问题不大。订单表ordersid、order_no业务订单号、user_id、total_amount、status、remark、address、consignee、phone、create_time、pay_time、confirm_time。注意表名不要叫orderorder是SQL关键字虽然加反引号能用但没必要给自己挖坑加个s最保险。订单明细表order_detailid、order_id、dish_id、dish_name、dish_image、price、quantity、subtotal。明细表的作用是固化下单那一刻的菜品快照。如果以后菜品改名、调价历史订单依然能还原当时点的是什么。3.2 订单状态机状态字段怎么流转订单状态是整个系统逻辑最复杂的部分。我定义了一组int状态码0-待支付、1-待接单、2-制作中、3-已完成、4-已取消。状态流转规则如下用户下单后状态为0待支付模拟支付成功后变为1待接单商家点击接单后变为2制作中商家点击完成或用户确认收货后变为3已完成。用户在下单后、商家接单前可以主动取消状态变为4已取消。这里要强调一个容易忽略的点状态变更不是谁都能改的。用户只能取消状态为0或1的订单商家只能把1改成2、把2改成3。如果不在Service层做状态校验用户传一个status3就直接把接口改了那整个系统就形同虚设。所以状态变更的逻辑必须写在后端前端只是发起点真正的状态机校验在后端完成。3.3 一个容易踩的坑金额字段到底用DECIMAL还是FLOAT这是我在这个项目里遇到的最经典的坑之一。如果你用float或double存金额用户付了0.58元MySQL里面存进去的可能是0.57999999999999998查询出来再算总价分分钟对不上账。浮点数在计算机中是二进制的很多十进制小数无法精确表示这不是MySQL的问题是所有编程语言通病。正确做法是金额全部用DECIMAL(10,2)Java实体类用BigDecimal避免使用double去接。BigDecimal之间做运算用add、subtract、multiply不要用运算符。这个细节写进项目里答辩时老师问“为什么金额用BigDecimal”你能从二进制浮点误差讲到数据库精度这道题就稳了。4. 后端实现从接口设计到MyBatis的使用细节后端这部分是最容易堆代码但最需要小心的。我按“接口划分、MyBatis关键写法、事务控制、常见坑”四个维度来复盘。4.1 RESTful接口怎么划分接口设计遵循RESTful风格以资源为中心。用户模块POST /api/user/register、POST /api/user/login。菜品模块GET /api/dish/list带分类ID可选参数、GET /api/dish/{id}。分类模块GET /api/category/list。购物车模块GET /api/cart/list、POST /api/cart/add、PUT /api/cart/update、DELETE /api/cart/{id}。订单模块POST /api/order/submit、GET /api/order/list、GET /api/order/{id}、PUT /api/order/cancel/{id}。管理端GET /api/admin/order/list、PUT /api/admin/order/accept/{id}等。权限上我用SpringBoot的HandlerInterceptor写了一个简单的JWT拦截器。用户登录成功后返回一个token前端每次请求放在请求头Authorization里拦截器校验token并解析出userId和role。管理端的接口再根据role做二次校验。不用引入Spring Security对于这个项目来说太重了而且配置难度会拖慢进度。4.2 MyBatis的XML映射和动态SQL写法MyBatis用XML写SQL这是它的灵魂。以菜品分页条件查询为例需要支持从分类ID筛选、关键词模糊查询、状态筛选三个可选条件。SQL写在DishMapper.xml里面select idselectPage resultTypecom.example.entity.Dish SELECT * FROM dish where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /selectwhere标签会自动去掉第一个多余的AND这个设计非常实用不用自己拼WHERE 11这种丑代码。动态SQL的if里面写OGNL表达式这是MyBatis最需要小心的点我下面单独说。订单和订单明细的一对多映射用collection实现。查询订单列表时一条订单要带上它的所有明细项resultMap idOrderWithDetailMap typecom.example.entity.Order id propertyid columnid / result propertyorderNo columnorder_no / result propertytotalAmount columntotal_amount / collection propertyorderDetails ofTypecom.example.entity.OrderDetail id propertyid columndetail_id / result propertydishName columndish_name / result propertyquantity columnquantity / result propertysubtotal columnsubtotal / /collection /resultMap这种映射的重点是SQL联查时明细表的主键必须起别名detail_id否则如果主表ID和明细表ID都叫idMyBatis会映射错乱。这个坑我踩过一次查出来的明细数量总是少了排查半天才发现是主键冲突。4.3 事务边界为什么下单这个操作必须加Transactional下单接口的逻辑是校验菜品是否存在且上架、计算总价、生成订单主记录、批量生成订单明细、扣减库存。这五个步骤必须在同一个事务里执行因为任何一个失败前面做的都不能生效。我在下单Service方法上加Transactional(rollbackFor Exception.class)。注意rollbackFor必须指定因为Spring的默认行为是只对RuntimeException回滚如果代码里抛的是IOException之类的受检异常事务不会回滚数据就出现半截状态。这个细节很多人不知道但面试和答辩都很喜欢问。扣减库存的SQL这样写UPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity}stock #{quantity}这个条件是我前面提到的防超卖关键。如果更新的影响行数为0说明库存不足直接抛出异常回滚整个下单事务。这样就算用户疯狂点击提交订单也不会出现库存负数的情况。4.4 MyBatis常见的两个坑单个数字字符比较和SQL日志配置先说单个数字字符比较这是MyBatis动态SQL里非常经典的一个坑。假设菜品状态status是Integer类型你在if里判断if teststatus 1看起来没问题但OGNL表达式中单引号包起来的1会被当作Character字符类型而status是IntegerInteger永远不等于Character这个条件就永远不会成立SQL里就拼不上这个条件。正确写法是if teststatus 1直接把1写成数字字面量。如果你非要写字符串就要写成if teststatus 1把单引号和双引号反过来。这个坑非常隐蔽代码不报错但条件就是不生效排查起来很费时间。再说SQL日志配置。开发阶段看不到MyBatis执行的SQL排查问题效率极低。在application.yml里加上mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl配置之后控制台会完整打印SQL语句和参数值。调试完联调阶段建议关掉不然日志刷屏影响看其他信息。5. 前端实现Vue层面如何组织和对接接口前端我用的是Vue2全家桶Vue Router做路由、Vuex管理购物车状态、Axios统一请求后端接口、Element UI做UI组件库。整体结构分成用户端页面和管理端页面两块。5.1 前端项目结构和路由设计项目创建用vue create命令选默认的babelroutervuex预设。目录结构按功能划分src/views放页面级组件用户端有MenuPage.vue点餐页、CartDrawer.vue购物车抽屉、OrderList.vue订单列表、OrderDetail.vue订单详情管理端有AdminLogin.vue、AdminHome.vue、DishManage.vue菜品管理、OrderManage.vue商家订单处理、CategoryManage.vue分类管理。路由需要在main.js里配置。用户端和管理端建议分成两个独立的布局用户端是“顶部导航左侧分类中间菜品列表右侧购物车”的结构管理端是“左侧菜单右侧内容区”的典型后台布局。可以给管理端路由统一加一个前缀/admin路由守卫里校验登录状态。{ path: /admin, component: AdminLayout, meta: { requiresAuth: true, role: ADMIN }, children: [ { path: dish, component: DishManage }, { path: order, component: OrderManage } ] }路由守卫在beforeEach里判断meta中的requiresAuth没有token就跳去登录页。有token但role不匹配就拦截住防止普通用户直接敲URL进管理端。5.2 axios封装和接口对接前端请求不要直接在每个页面里写this.$axios.get那样改接口地址要改到崩溃。统一封装一个request.js做三件事创建axios实例并设置baseURL请求拦截器里把token加到请求头响应拦截器里统一处理错误码和401跳转。import axios from axios const request axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response response.data, error { if (error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default requestbaseURL用环境变量控制开发环境走http://localhost:8080/api前后端联调时还会遇到跨域问题后面会细说。5.3 购物车状态管理和组件通信购物车是点餐页最核心的交互。用户点击“加入购物车”后顶部导航的购物车角标要立刻更新右侧购物车抽屉也要能同步显示最新的数据。这种跨组件的状态同步用Vuex的Module来管理最合适。定义cart模块的state是cartList数组mutations里有addToCart新增或累计数量、updateQuantity、removeFromCart、clearCart。点餐页的菜品卡片组件不直接操作Vuex而是通过this.$store.commit(cart/addToCart, dish)提交mutation购物车抽屉组件通过mapState读取cartList自动响应更新。组件通信还有一个细节菜品卡片点击的对象是整个dish对象Vuex里计算购物车总价时要用cartList.reduce遍历累加每一项的price * quantity金额计算一定用整数分来算展示的时候再转成元避免浮点误差。我在前端统一用分作为计算单位接口返回的金额也是以分为单位的整数彻底绕开浮点问题——这是后端也能做但前端更需要坚持的约定。5.4 前后端联调时的三个经典问题第一是跨域。前端跑在8081端口后端跑在8080浏览器会拦截跨域请求。解决方案有两种后端加CrossOrigin或配置CorsFilter全局跨域或者前端在vue.config.js里配devServer的proxy代理。我推荐前端代理的方案devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/dish/list会被代理到后端的/api/dish/list浏览器看到的始终是同源的不需要后端做任何跨域配置上线部署也更安全。第二是时间格式。MySQL的datetime字段Jackson默认序列化成2024-01-15T10:30:00这种带T的格式前端显示很不友好。统一在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三是IP配置的问题。如果你的本机IP不是localhost后端接口地址写死了localhost手机访问项目时就会全部失败。联调时建议把前端环境变量里的VUE_APP_BASE_API留空通过代理转发这样后端地址只管在vue.config.js里改一处就行。6. 部署、打包和那些文档里不会写的坑项目写完不是结束能打包部署起来才算真正完成。这个环节的坑比写代码时遇到的还要多。6.1 环境版本的匹配问题这个项目最大的环境坑是版本匹配。我踩过最痛的一次是SpringBoot拉到3.0版本但电脑装的是JDK8结果mvn spring-boot:run直接报错提示Class文件版本错误。SpringBoot 3.x强制要求JDK17Maven编译器插件也会检查Java版本版本不匹配根本无法启动。同理前端Node版本也会影响。Vue2项目如果Node版本过高比如Node 18以上执行npm run build时可能报错Digital Envelope Routines::unsupported这是OpenSSL版本变化导致的需要指定NODE_OPTIONS--openssl-legacy-provider才能打包或者降Node版本到16。我建议直接装一个Node版本管理工具方便切换版本。数据库方面MySQL 8.0的JDBC驱动要把URL写成jdbc:mysql://localhost:3306/db?serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue不加allowPublicKeyRetrieval有时会报Public Key Retrieval错误这个坑遇到过的人应该不少。6.2 前端打包后布局异常和资源404的问题npm run build生成dist目录之后双击index.html打开页面要么白屏要么样式全乱、图片全挂。两个原因一是Vue Router的mode是history静态文件服务器没有做路径重写访问/admin/dish返回404。二是打包后的资源路径是绝对路径/js/app.js直接file协议打开必然失败。解决方案Vue Router改用mode: hash这样URL里会带#任何路径都不会404打包配置设置publicPath: ./让资源加载使用相对路径。如果你有静态服务器环境也可以不绕hash模式而是在Nginx里配置try_files $uri $uri/ /index.html但本地演示用hash模式最省心。我在实际部署时用的是hash模式加相对路径dist目录直接扔到Tomcat的webapps下就能跑不用额外配Nginx对毕设演示来说足够了。6.3 后端打包Maven打JAR包和运行时的注意事项后端打包用mvn clean package -DskipTests生成target目录下的JAR文件。直接在命令行运行java -jar xxx.jar就能启动。这里有两个注意点如果SpringBoot内置了Tomcat默认端口是8080需要确认没有端口冲突JAR包里的配置文件默认读取的是src/main/resources下的application.yml如果打包后想改数据库密码不能直接改JAR内部文件要用外部配置覆盖java -jar xxx.jar --spring.config.additional-location/path/to/application.yml或者用环境变量注入SPRING_DATASOURCE_PASSWORD。这个技巧对部署环境切换非常实用不用每次改配置都重新打包。前端dist目录还可以尝试直接复制到SpringBoot的src/main/resources/static目录下然后重新打包这样就能用同一个SpringBoot进程同时提供接口服务和前端页面。这是最简单的一体化部署方案缺点是前后端耦合了但用于演示和答辩一个JAR跑起整个项目展示效果格外加分。7. 写在最后的经验补充分享项目做下来我最大的体会是这类全栈项目真正的难点不在某个框架的API怎么调而是把数据库设计、事务边界、接口规范性、前后端配合这些“工程感”的东西搞明白。你写出一个能跑的项目不算本事能让别人接手你的代码时不用猜逻辑答辩时每个表、每个接口、每个状态流转都能讲出为什么这才叫做完了一个项目。再分享一个我自己很受用的技巧每张业务表都加上create_time和update_time字段MySQL里用DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP自动维护Java实体里对应LocalDateTime。别小看这两个字段排查数据问题、做数据统计、展示到前端处处都能用到。如果你做完这个点餐系统还想继续扩展方向可以考虑给购物车换成Redis存储并加个过期时间订单提交改成RabbitMQ异步处理菜品图片上传改用OSS对象存储数据统计加上ECharts图表展示。这些升级方向都不复杂但对技术成长和简历含金量的提升非常明显。整套流程走下来我从MySQL建表逻辑到Vue组件设计、从MyBatis动态SQL到SpringBoot打包部署过了一遍完整的全栈开发链路。希望这篇记录也能让你的项目之路少走几个弯路。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询