资讯动态

Spring Boot + Vue餐厅点餐系统:前后端分离全栈开发实战解析

发布时间:2026/10/16 5:27:02 来源:尧图企业网站定制
做过不少类似的互联网项目但餐厅点餐这种业务场景简单、角色清晰、又很贴近大家日常生活的系统其实特别适合拿来完整梳理一遍前后端分离的开发思路。这个基于Spring Boot Vue的餐厅点餐系统麻雀虽小但五脏俱全。顾客端能看菜、下单、结算管理端能管菜品、管分类、管订单一个典型的全栈闭环。如果你是正在准备毕设、或者刚学完框架想找个练手项目又或者是中小餐厅想了解数字化点餐怎么落地这篇内容会把从技术选型到数据库设计再到前后端联调和部署的完整链路都摊开说清楚。我写的时候尽量按照实际开发流程来把当时踩过的坑、做过的取舍、踩完坑后的排查思路一并记录下来方便你少走点弯路。1. 项目定位与技术选型思路1.1 需求边界到底要做成什么样先别急着写代码第一步是把需求边界划清楚。餐厅点餐系统听起来简单但不同的场景下需求差异很大。快餐店要的是出餐快、桌台少正餐厅要的是桌台管理、并桌拆桌外卖场景要接入配送。我们做的是最经典的堂食点餐模型再加一个管理后台。这个系统的核心角色就两个顾客和管理员。顾客端是H5页面扫码或者打开链接就能用不需要下载App这是为了贴合餐厅场景的低门槛要求。管理员端是Web管理界面在电脑上操作。功能边界上顾客端聚焦四件事浏览菜品、加入购物车、提交订单、查看订单状态。管理员端是另外四件事管理菜品分类、管理菜品信息、处理订单状态、查看基础统计。这里有个重要的取舍。一开始有人提过要做会员积分、优惠券、分享裂变我全部砍掉了。原因很简单MVP最小可行产品的核心目标是跑通点餐主流程。会员积分看似简单但它牵扯到账户体系、流水记录、过期策略复杂度会滚雪球。先做核心闭环后续再迭代是很健康的做事方式。1.2 技术栈组合Spring Boot Vue为什么是黄金搭档选技术栈的时候我几乎没有犹豫就定了Spring Boot Vue的组合。理由有三点这三点在今天依然成立生态成熟度极高。Spring Boot已经成为Java后端事实上的标准无论是资料数量、社区活跃度还是和各种中间件的整合便利性都很难找到替代品。Vue在国内前端圈子的普及率也相当高尤其适合我们这种中小型管理系统的开发。前后端分离带来的开发效率提升。后端只负责提供RESTful API前端页面完全独立开发。团队协作时两边只需要对齐接口文档不用再像JSP时代那样前后端耦死在一起。哪怕你是单人开发分离架构也能让你把逻辑边界理得更清楚。招聘和学习成本可控。这套栈几乎是Java开发者的标配技能后续接手维护的人上手成本低。相比一些更小众但看似更酷的方案这个选择稳得多。ORM层选了MyBatis-Plus而不是原生MyBatis或JPA。实际体验下来MyBatis-Plus的代码生成器帮了大忙单表CRUD几乎不用手写SQL复杂查询靠Wrapper条件构造器就能搞定。JPA虽然写起来更优雅但在复杂报表统计时原生SQL的灵活性还是更让人放心。1.3 基础设施选型数据库、缓存、对象存储的取舍数据库用的MySQL这个不解释经典场景配经典存储。缓存用的Redis主要承担两方面工作一是存验证码二是缓存首页菜品列表。对象存储这块最开始图省事把菜品图片直接传到了服务器本地磁盘后来发现这样有几个隐患——图片无法通过HTTPS访问、服务器重启可能丢文件、磁盘空间被撑爆。后来改成了MinIO搭建私有对象存储既能自己掌控数据又能有S3协议兼容性餐厅数据量下完全没有问题。存储选型上有一条经验值得展开说一下。菜品的图片、Logo这些静态资源一定要走独立存储不要和代码包混在一起。当时我在本地环境图片一切正常一部署到服务器图片全部404。排查了半天原因是Ubuntu上路径大小写敏感和目录权限都出了问题而Windows上完全感知不到这些问题。独立存储方案可以从根上规避这一坑。2. 数据库设计点餐系统的心脏2.1 从业务模型到ER图的推导过程数据库设计是整个系统里我耗时最长、也认为最重要的一环。我不太喜欢直接看现成的表结构而是习惯从业务链路自己推导一遍。第一步拆业务动词。顾客这个角色涉及的动作有看菜查菜品、加购操作购物车、下订单写订单主表、付钱模拟支付更新订单状态。管理员涉及的动作有改菜单菜品增删改、改分类、处理订单接单、完成、取消。第二步找核心实体。从动作里能提炼出五个核心实体用户顾客、菜品分类、菜品、订单、订单明细。购物车在数据库里没有独立表因为购物车本质上是一个临时状态用Redis或者前端LocalStorage就够不需要持久化。这是很多人设计数据库时容易犯的错——把临时状态也设计成持久化表事后又要面对数据同步的问题。第三步定义实体关系。一位顾客可以拥有多张订单一个订单包含多个菜品因此需要订单主表和订单明细表来做1:N的拆分。菜品和分类是多对一关系一个分类下挂多个菜品。用户和订单是一对多关系。订单和订单明细是一对多关系。关系确定后外键字段也就明确了。2.2 六张核心表的字段设计和约束说明建表的时候我对字段做了一些刻意的规范。拿订单主表举例它的核心设计要点是这样的CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, order_no varchar(32) NOT NULL COMMENT 订单编号业务唯一, user_id bigint NOT NULL COMMENT 下单用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额单位元, status tinyint NOT NULL DEFAULT 0 COMMENT 订单状态0待支付 1已支付 2制作中 3待取餐 4已完成 5已取消, remark varchar(255) DEFAULT NULL COMMENT 顾客备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT订单主表;几个设计细节值得解释订单号设计成业务唯一键而不是直接用主键ID。原因很现实——主键ID是自增的一旦订单量上来竞争对手通过接口就能推算出你的日订单量这个信息没必要暴露。我用的订单号生成规则是时间戳到秒 用户ID后四位 四位随机数比如2025060715301234567890这种形式确保并发下不重复。金额用decimal(10,2)而不是float或double。这是Java后端开发的老生常谈但依然有人反复踩坑。二进制浮点数在加减时会有精度损失比如4.58 0.01 在某些情况下可能算出4.589999999。金额这种东西差一分钱都是事故必须用精确类型。订单状态用tinyint而不是varchar存中文。扩展性更好状态流转可以通过数值大小判断。团队里有人觉得存中文更直观但我坚持用数字因为业务判断永远走的是逻辑分支不是给人看的展示展示层由前端枚举映射搞定。菜品表这边有一个字段值得一提就是status上架状态。这个字段比很多人想象的更重要——它不是用来给管理员看的而是用来做逻辑删除的。餐厅换菜单是常态如果下架的菜品直接从数据库删掉那历史订单里的明细关联就会出现悬挂记录。保留数据、只翻转状态是对数据完整性的保护。CREATE TABLE dishes ( id bigint NOT NULL AUTO_INCREMENT COMMENT 菜品ID, category_id bigint NOT NULL COMMENT 所属分类ID, name varchar(64) NOT NULL COMMENT 菜品名称, description varchar(255) DEFAULT NULL COMMENT 菜品描述, image_url varchar(255) NOT NULL COMMENT 图片URL, price decimal(10,2) NOT NULL COMMENT 单价元, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1上架 0下架, sort int NOT NULL DEFAULT 0 COMMENT 排序值越小越靠前, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT菜品表;订单明细表和菜品表之间有一个微妙的关联处理我没有设置外键约束。这看起来和教科书不同但实际是对的。订单明细属于快照数据顾客下单那一刻的菜品价格、名称、图片都要固化到明细表里。如果菜品后来涨价或改名历史订单显示的还是下单时的信息这是业务需要。而外键约束会阻止菜品被修改反而碍事。用逻辑关联替代物理外键性能也更好避免每次插入明细都要校验外键。2.3 索引与查询优化的实战细节建完表别急着写接口索引设计同样关键。点餐系统的查询模式其实很固定无外乎这几类顾客查菜品列表按分类查、管理员查订单列表按状态查、运营看统计按时间聚合。联合索引的设计原则是等值查询列在前范围查询列在后。比如订单列表的常用查询条件是status加上时间范围那么建索引(status, create_time)比单独建两条索引效率高得多。MySQL的B树索引特性决定了联合索引能同时过滤两个维度而两个单列索引最多只能用一个。这里有个反直觉的点orders表我建了idx_user_id和idx_status两个独立索引但并没有建(user_id, status)联合索引。原因是业务上某个用户的所有订单和所有用户的某种状态订单是两个完全不同的使用场景没有同时以user_id和status为过滤条件的查询需求。索引不是越多越好每一份索引都占用磁盘空间每次写入都要维护需要在查询效率和写入性能之间做平衡。菜品列表的缓存策略也值得讲一讲。首页的菜品列表是访问最频繁、变化最少的数据非常适合用Redis做缓存。但缓存不是简单的读的时候先查缓存还要考虑缓存失效问题。我用的策略是管理员操作菜品或分类时主动删除相关缓存key而不是设置很短的过期时间等它自然过期。这属于经典的主动失效 被动过期双保险模式。当时做这个设计时还专门模拟过极端场景比如管理员在顾客下单过程中更新菜品图片会不会导致用户端界面异常。实测结果是顾客刷新页面后看到的是新图片和新的价格订单里的快照数据不受影响体验上是完全可以接受的。3. 后端核心模块实现与联调细节3.1 项目结构划分和启动引导类配置后端项目结构我采用经典的分层架构Controller受理请求、Service处理业务、Mapper访问数据库。不要小看这种基础分层它能强制你思考代码归属地避免业务逻辑散落在各个角落。com.example.restaurant ├── controller # 接口层只做参数接收和结果封装 ├── service # 业务层核心逻辑都在这里 ├── mapper # 数据访问层MyBatis-Plus生成的BaseMapper ├── entity # 数据库实体类与表结构一一对应 ├── dto # 数据传输对象接收前端入参 ├── vo # 视图对象返回给前端的数据 ├── config # 配置类Redis、拦截器、跨域等 ├── common # 公共工具类和统一返回结果封装 └── exception # 全局异常处理配置项目文件的启动依赖时围绕Spring Boot的版本踩过一个比较大的坑。当时最新稳定版是3.x但很多中间件客户端还没跟上比如某些版本的Redis客户端和MinIO SDK对Java 17的兼容性还有问题。后来我都统一用了2.7.x版本配合Java 8生产环境上兼容性稳稳当当。选版本这件事不能盲目追新稳定性和生态兼容性优先。跨域配置也是必须处理的一步。前端Vue跑在5173端口后端跑在8080端口浏览器同源策略会把两者之间的请求直接拦掉。我用CorsFilter解决Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意allowedOriginPattern和allowedOrigin的区别。在Spring Boot 2.4之后如果你要设置allowCredentials(true)再使用addAllowedOrigin(*)会直接报错因为浏览器不允许带凭证的请求使用通配符Origin。用allowedOriginPattern(*)可以规避这个问题。这个细节很小但不注意就会被卡很久。3.2 JWT登录鉴权和拦截器链路登录鉴权在点餐系统里分成两种场景。顾客端是免登录的扫码进入直接选菜管理员端需要登录。这个设计是业务决定的——餐厅点餐追求的是进来了就能点如果顾客还要先注册登录转化率会断崖式下跌。管理员后台必须鉴权因为涉及经营数据。使用JWTJSON Web Token的原因很务实它是无状态认证服务器不需要保存session特别适合前后端分离架构。用户登录成功后后端签发一个token前端存在localStorage每次请求把token放在请求头的Authorization字段里。后端用一个拦截器统一校验。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行OPTIONS预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token) || !JwtUtil.verifyToken(token)) { response.setStatus(401); return false; } // 解析出用户ID存入request属性后续业务可以直接取用 request.setAttribute(userId, JwtUtil.getUserId(token)); return true; } }这里有个实操经验token过期时间的设置要结合餐厅场景。管理员可能上午登录下午就不管了设太长不安全设太短又影响使用。我最后设的24小时然后在前端封装了401拦截token失效就跳转登录页。顾客端不依赖这个拦截器通过配置放行路径解决。JWT本身有个风险点是要注意的就是它无法在服务端主动作废。如果管理员的账号泄露了在token到期前它都是有效的。为此我做了一个折中方案——在用户表中记录一个token签发时间戳校验token时对比签发时间和当前时间超过N小时强制失效。这相当于给JWT加上服务端刷新机制弥补无状态认证的短板。3.3 菜单和分类模块从增删改查到缓存策略菜品管理是管理端最常用的功能写法和常规CRUD没太大区别但缓存策略需要精细设计。菜品新增和修改的请求体里包含图片URL、价格、描述、分类ID等字段。我的做法是Controller接收DTOService层先做参数校验比如价格必须大于0再转换为实体类入库最后删除菜品缓存。微妙的是缓存删除的时机。一开始我把缓存删除放在Controller层后来发现Controller越写越肥而且如果多个接口都改了菜品每个都要手动删缓存很容易遗漏。后来统一放在Service层用一个CacheEvict注解或者手动调用redisUtil.delete(key)搞定。缓存逻辑的归属要清晰推荐放在Service层。菜品列表的查询走的是三级链路先查Redis没有则查数据库查完回填Redis。但有一个坑必须提醒用MyBatis-Plus做分页查询时如果返回的对象不是实体类而是VO就容易出现数据映射错误。我的做法是直接用XML写自定义SQLJOIN分类表查出分类名映射到VO。虽然稍微繁琐一点但语义清晰不会出现字段名对不上的诡异问题。还有一个体验细节排序值的应用。菜品表和分类表都设计了sort字段管理员在后台拖拽排序后前端就按这个字段排序展示。这种做法比按创建时间排更可控餐厅可以把招牌菜排在前面把时令菜置顶。3.4 订单模块状态机设计与并发控制要说整个系统最核心的部分还得是订单模块。点餐系统的一切都是围绕订单跑的。订单状态机我是精心设计的。参考了正规餐厅的发餐流程拆成五个状态待支付 - 已支付 - 制作中 - 待取餐 - 已完成外加一个终态已取消。取消在什么阶段允许发生待支付时顾客可以自己取消已支付后只能由管理员取消做退款标记。这个规则看起来自然但真正落地时还是容易漏掉分支判断好在用状态机图理清了转移条件写代码时就顺畅多了。创建订单是并发控制的重灾区。传统的先查库存再扣库存的做法在并发下会超卖比如最后一份招牌菜同时被两个顾客看到两个人都下单成功了。我在dish表里加了一个stock字段然后用乐观锁机制UPDATE语句带版本号条件保证扣库存的原子性UPDATE dishes SET stock stock - 1, version version 1 WHERE id #{dishId} AND stock 0 AND version #{version}如果影响行数为0说明库存已被扣光或者版本冲突直接抛异常让用户重试。这样就把并发问题挡在了数据库层不需要引入分布式锁这种重型武器。订单编号生成一开始用的是简单的UUID去横线后来发现两个问题一是太长不适合在取餐屏上快速念出来二是纯数字不好排版。换成了时间戳随机数方案后顾客报号取餐方便很多。还加上了校验位抄号时如果抄错一位能被自动发现。3.5 支付模块模拟支付但预留真实接口真正对接微信支付需要商户号和企业资质个人开发者没有这个条件。所以支付模块我设计成了模拟支付 预留真实接口的模式。前端点击去支付按钮后弹出一个收银台模态框展示一个假的付款二维码提示用户模拟支付点击确定即支付成功。点击确定后前端调后端接口后端把订单状态翻转为已支付同时触发向厨房打印机推送制作通知的逻辑。接口设计上留了一个支付回调的扩展点public interface PaymentService { PayResult pay(PayRequest request); // 发起支付 PayResult callback(PayNotify notify); // 处理支付回调对接真实支付时实现 }将来如果拿到真实商户号只需要增加一个WxPayServiceImpl实现类把模拟逻辑替换成微信支付的统一下单和回调验签即可。业务层代码完全不用动这是接口隔离带来的好处。这个设计思路建议有类似模拟需求的项目都参考一下。很多毕设项目写着写着就陷入模拟就是假的假的就不用设计的误区其实不管是真是假接口的边界都应该划清楚这样才有后续演化的可能。4. 前端工程化实践与页面交互4.1 从零搭建Vue工程目录结构和路由设计前端由两大部分组成顾客端H5和管理后台Web。共用一套工程代码用Vue Router的路由配置区分访问入口。顾客端的路由设计围绕快速点餐这个核心目标/c 首页菜单列表 分类切换 /cart 购物车页 /orders 我的订单用手机号查询 /orders/:id 订单详情这里有一处设计取舍要注意顾客端没有做登录注册那我的订单怎么查询我的方案是手机号作为取单凭证。顾客填手机号下单查订单时输手机号加验证码即可。这是很多中小餐厅的真实做法因为堂食场景下没人愿意注册账号手机号验证码已经是数据收集的极限了。验证码存Redis设置5分钟过期后端用阿里云短信接口发送。考虑到短信成本每次点击发送前都有60秒倒计时限制。管理后台的路由分两大块/admin/dashboard数据看板、/admin/dishes菜品管理、/admin/categories分类管理、/admin/orders订单处理。导航守卫是必须写的。管理后台所有路由都加meta: { requiresAuth: true }全局前置守卫里检查有没有tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.matched.some(record record.meta.requiresAuth) !token) { next(/login) } else { next() } })方法虽然简单但实测够用。要注意的是仅凭本地有token并不能保证用户真的有效所以我在Axios的响应拦截器里加了401处理后端校验不通过时强制跳登录。4.2 Axios封装和接口层的最佳实践前后端分离项目里Axios封装几乎是一个固定的步骤。我当时封装的粒度是把每个模块的接口都抽成独立文件然后在组件里按需引用// api/dish.js import request from /utils/request export function getDishes(params) { return request({ url: /api/dish/list, method: get, params }) } export function addDish(data) { return request({ url: /api/dish, method: post, data }) }request对象是我封装好的实例统一配置了baseURL、超时时间和请求拦截器const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) service.interceptors.response.use( response { const res response.data // 后端统一返回 { code, data, msg } 结构 if (res.code ! 200) { ElMessage.error(res.msg) return Promise.reject(new Error(res.msg)) } return res.data }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这里有一个细节环境变量的使用。开发环境后端地址是http://localhost:8080生产环境可能是域名如果在每个请求里硬编码就完了。通过.env.development和.env.production区分环境变量打包时自动替换可以直接避开这个问题。4.3 购物车状态管理的设计购物车模块我选了用Vuex/Pinia来做状态管理而不是在每个页面组件里各自维护一个数组。原因很实际菜单页加菜、购物车页改数量、导航栏角标显示总件数这些分散在不同页面的状态必须共享。Pinia Store的购物车设计大概是这样的export const useCartStore defineStore(cart, { state: () ({ items: [], // { dishId, name, price, image, count } remark: }), getters: { totalCount: (state) state.items.reduce((sum, item) sum item.count, 0), totalAmount: (state) state.items.reduce((sum, item) sum item.price * item.count, 0) }, actions: { addItem(dish) { const existing this.items.find(item item.dishId dish.id) if (existing) { existing.count } else { this.items.push({ dishId: dish.id, name: dish.name, price: dish.price, image: dish.imageUrl, count: 1 }) } }, clearCart() { this.items [] this.remark } } })购物车数据没有放到后端。因为购物车数据本质上是临时且私有的存后端既要维护购物车表又要有合并逻辑而存localStorage之后用户关掉浏览器再打开购物车还在体验反而更好。把临时性数据下沉到本地存储把承担资金流水的数据放到后端持久化这个分层依据里藏着很多项目的决策智慧。还有一点加入购物车时菜品价格和下单时菜品价格可能不一致。如果在购物车里存价格用户加购后管理员调价了结算时以哪个为准我的做法是下单前用菜品ID重新查一次当前价格购物车里的价格仅作展示用。这就是前面数据库设计里快照数据思想的应用。4.4 移动端适配与餐厅场景的交互优化顾客端是给在餐厅里拿着手机扫码的人用的移动端适配马虎不得。我没有用rem或vw做全面适配而是用了viewport 响应式布局因为餐厅点餐的页面结构不算复杂flex布局配合max-width: 480px居中基本可以覆盖主流手机尺寸。像素级适配的坑主要体现在输入框和按钮上。iOS下有默认的内边距Android上字体缩放不同这些细节不对齐就会显得页面粗糙。给按钮和输入框统一设置-webkit-appearance: none可以解决大部分问题。餐厅场景有个特殊的交互要求出餐快、等待少。所以顾客端页面的加载速度至关重要。我做了几项优化菜品图片用懒加载v-lazy列表数据滚动到可视区域才加载分类切换不重新请求接口而是前端过滤已加载的菜品数据首页骨架屏提升首屏加载感知速度。这些优化对转化率的影响是实打实的——等3秒以上很多顾客已经转头去问服务员了。4.5 管理后台的数据可视化用轻量方案替代重框架管理后台的Dashboard本来想上ECharts全家桶后来自我否定了。原因是餐厅老板关心的问题其实很聚焦今天卖了多少单、哪个菜卖得好、哪个时间段是高峰。三个核心问题不需要引入几十KB的图表框架。我用轻量方案解决纯CSS写柱状图。后端返回每个菜品的销量排行前端用一个div加宽度百分比渲染长度再配一个时间段的订单量表格。不用图表库反而加载更快、视觉更清爽。只有当需要折线图和复杂交互时ECharts才值得上场。技术选型以场景为准这个原则放到前端同样适用。5. 环境部署、联调与排坑实录5.1 本地开发环境的搭建步骤想在本地把项目跑起来按下面这套步骤操作大概率一次通过安装基础环境JDK 8、Maven 3.6、MySQL 5.7、Redis 6、Node.js 16。初始化数据库执行项目里提供的init.sql脚本创建数据库和表同时写入测试分类和菜品数据。修改后端配置调整application.yml中的MySQL账号密码、Redis地址、MinIO的bucket和密钥。启动后端服务在项目根目录运行mvn spring-boot:run看到Started Application日志说明启动成功。启动前端项目进入Vue目录执行npm install完成后执行npm run dev。验证连通性浏览器打开http://localhost:5173如果是管理端则访问/admin路由登录跳转正常即为联调成功。值得注意的是后端端口和前端端口不要搞混。前端Vite默认5173后端Spring Boot默认8080两者通过代理通信。Vite配置文件里设置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这个代理配置解决了开发阶段的跨域问题而且前端代码里的请求地址不用写绝对路径。这个开发体验方案在后端上线时也能平滑迁移让前后端联调环境与生产环境在路由语义上保持一致对后续排查线上问题也很有帮助。5.2 数据库初始化脚本的隐藏技巧init.sql脚本不只是建表还要内置初始管理员账号和演示数据。密码我存储的不是明文而是BCrypt加密后的哈希串。这一点很重要——数据库泄露时管理员的密码如果也是明文就等于把经营数据大门钥匙一起交了出去。演示数据要贴近现实。我准备了几个分类招牌热菜、凉菜、汤羹、主食、饮品。每个分类下面挂三到五个菜品价格从几块到几十块不等。为什么要有真实感强的数据因为开发和演示时如果全是什么测试菜1测试菜2管理后台的界面效果大打折扣也看不出布局是否协调。模拟数据的质量直接关系到前端调试的体验。5.3 常见问题排查七个高频坑位坑位一数据库连接报错Public Key Retrieval is not allowed。MySQL 8.x版本在连接时默认使用caching_sha2_password认证JDBC驱动首次连接需要去服务器拿公钥。解决办法是在JDBC URL后加上allowPublicKeyRetrievaltrue。这个报错能卡住很多人因为它不是报密码错误而是报一个看上去很莫名的公钥问题。坑位二Maven依赖下载缓慢或失败。国内环境直接走中央仓库经常慢到怀疑人生。我直接把settings.xml里的镜像源换成了阿里云镜像。这一步做完构建速度能提升数倍属于一次配置永久受益的操作。坑位三前端npm install出现ERESOLVE错误。这类错误大多是依赖版本冲突导致的多发生在不同Node.js版本混用的情况下。稳妥的做法是删除node_modules和package-lock.json后重装或者切换Node版本到项目的指定版本。一个通用建议锁定团队Node.js版本用.nvmrc文件标明避免在我电脑上是好的这种问题。坑位四图片上传成功但访问404。排查思路分两层。首先看MinIO控制台该路径下有没有这个对象其次看访问路径拼接是否正确。当时还很愚蠢地把endpoint的端口写错了一位导致访问地址跳到了一个不存在的服务端口。图片类资源的问题排查时先确认存储服务本身通不通再看路径拼写。坑位五后端接口返回时间格式是数组而非字符串。这是Jackson的一个默认行为时间序列化时会转成类似[2025, 6, 7, 15, 30, 0]的数组结构。前端拿到这种数据还要自行拼接极其不方便。在application.yml里配好全局时间格式可以彻底解决spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8坑位六长文本内容前端显示换行不生效。餐品描述里写了换行页面上却挤成一段。原因在于HTML把连续空白字符折叠了。用white-space: pre-line样式展示即可保留换行又不影响段落间的多余空格。坑位七大量菜品图片导致页面加载变慢。这个问题的根本解法是压缩图片。我用MinIO的缩略图API动态生成小尺寸图片列表页加载缩略图、详情页加载原图。这个方案比前端统一压缩更好因为不占用户CPU而且同一张图一次生成、多次复用。5.4 部署上线从打包到运行的完整链路部署方案我推荐Nginx 单机Spring Boot的方式适合餐厅这种中小流量场景。前端打包npm run build产物会输出到dist目录直接拷贝到服务器的/usr/share/nginx/html下。Nginx配置的关键点在静态资源路由和API反向代理server { listen 80; server_name your-domain.com; # 前端页面 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # Vue Router history模式必须 } # 后端API反向代理 location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }Vue Router如果用了history模式try_files那行配置至关重要。不加的话刷新页面时Nginx会去找对应的物理路径找不到就404。try_files把所有未知路径都重写到index.html由前端路由接管。后端打包mvn clean package -DskipTests生成的jar包用nohup java -jar启动或者最好配成systemd服务。后台运行、开机自启、异常自动重启一次解决。不要直接用java -jar挂前台SSH窗口一关服务就没了。5.5 性能优化Redis缓存与数据库压力缓解餐厅点餐的高峰时段非常集中尤其是午餐和晚餐两个饭点。数据库在这段时间内会承受很大的查询集中压力性能优化就成了必须考虑的事情。菜品列表缓存是老生常谈但缓存能扛的压力远比很多人想象中大。我压测过一次没有任何缓存时100个并发请求菜品列表MySQL的CPU占用率飙到40%多响应时间冲到800毫秒以上。开启Redis缓存后同样100个并发响应时间稳定在15毫秒以内。这个量级的提升对餐厅场景来说完全是质变。订单创建这块暂时不需要缓存介入因为写操作没法缓存而且订单量在餐厅单店场景下完全可控。压测数据显示单机MySQL每秒可以处理几千次insert真实餐厅一秒也就几个订单根本打不到瓶颈。性能优化不能漫无目的地做要先分析和压测再针对瓶颈动手。6. 测试覆盖与代码质量保障6.1 接口自测和Postman调试的要点开发过程中每个接口都必须用Postman跑通才算完成。我建了一个完整的Postman Collection按模块分组每个接口配好示例参数。有几个重要调试点分享一下Content-Type必须和后端对齐。Postman的POST请求如果提交application/json后端就必须用RequestBody接参如果是form-data后端要用RequestParam。很多人联调失败不是代码写错而是媒体类型没对齐。鉴权请求调试时不要先全部设好Header。我习惯先把接口调通再手动加Authorization测试鉴权分支这样可以确认401拦截逻辑是不是真的生效了。接口文档的及时更新非常重要。前后端分离开发接口的返回结构一旦变了另一方不能及时知道就需要花费大量时间沟通。用OpenAPISwagger自动生成文档可以省去手动维护的烦恼后端写好注解前端直接在Swagger页面确认参数非常方便。6.2 单元测试覆盖哪些核心场景单元测试主要覆盖了订单状态机、价格计算、验证码校验这几个核心逻辑。订单状态机的测试是最有价值的因为状态流转分支多很容易漏掉某个非法流转的校验。比如已完成的订单不能再次改成待支付这种异常路径一定要有测试保护。MockMvc写Controller层的接口测试MockBean掉Service层保证测试只验证接口参数的绑定和校验逻辑。数据层的测试用H2内存数据库跑SQL脚本验证MyBatis-Plus的SQL语法没有低级错误。写测试的真实目的是给未来重构兜底。项目迭代过程中谁能保证自己改坏哪块逻辑没有测试的代码库改起来如履薄冰。6.3 前端组件复用和代码规范Vue组件的划分我是按业务功能来的DishCard.vue菜品卡片、CategoryTabs.vue分类标签、CartBar.vue底部购物车栏、OrderStatusTag.vue订单状态标签。划分原则是一个组件只负责一块业务显示自己能完成的事情不要拖其他组件下水。代码规范方面吃过的亏是团队成员随手console.log随手提交调试代码留在生产环境很危险不仅暴露内部信息还会拖慢运行速度。我在ESLint配置里加了no-console规则强制清理调试用的输出。7. 常见问题排查速查表问题现象可能原因解决方案前端访问后端接口报跨域错误后端未配置CORS增加CorsFilter全局配置登录后刷新页面又跳到登录页路由守卫未校验token存在性或token存错位置统一从localStorage读取token守卫中判断后再放行图片上传成功但访问404MinIO端口/路径拼接错误检查endpoint地址和bucket名称MySQL连接报Public Key错误8.x版本认证机制变更JDBC URL加allowPublicKeyRetrievaltrue菜品列表请求很慢数据库查询没有走缓存增加Redis缓存管理员操作时主动失效订单状态一直显示待支付前端未正确调用支付回调或状态更新接口未成功检查支付接口返回状态码和订单状态更新逻辑价格出现小数误差使用了float/double存储金额改为decimal(10,2)管理端上传菜品后列表不更新修改缓存后未删除相关key在Service层统一删除菜品缓存移动端点击输入框页面放大viewport未正确配置添加meta nameviewport contentwidthdevice-width, initial-scale1.0打包后前端路由刷新404Nginx未配置try_files增加try_files $uri $uri/ /index.html;配置8. 从项目到上线一些特别的体会最后聊聊我从这个项目里得到的、教科书上不会写的经验。技术选型要围绕谁在用来思考。做餐厅点餐系统使用者可能是五十多岁的餐厅老板他对技术先进完全无感在乎的只有上菜快不快、结账乱不乱。所以你的系统页面要少、按钮要大、流程要短。需求评审时每一个功能都该问一句这个功能对这位真实用户有什么价值没有价值的功能再炫酷也应该砍掉。主流程的可靠性远远重要于边缘功能的丰富度。点餐-下单-支付-出餐这条链路每一步都不能出问题。我当时花了一半以上的时间在打磨这条主链路的异常分支上库存不足能不能提示准确订单状态流转不合法时能不能被拦截支付超时会不会导致订单卡死这些场景表面上看频率不高但一旦发生就是重大事故。一个小餐厅积累口碑需要一年订单出错丢口碑只需要一单。文档虽然不是代码但价值不比代码低。数据库设计文档、接口文档、部署手册我事后回过头看幸亏都写了。两周前改的一个字段如果没看文档可能就要翻半天代码才能恢复记忆。毕设或者练手项目建议养成同样的习惯写文档的时候就是你重新梳理系统逻辑的时候经常能发现隐藏的问题。餐厅点餐系统这个项目做完最大的感受是好的系统未必用了多高深的技术但一定要把业务理解透把基础方案做扎实。Spring Boot Vue这套组合之所以经典正因为它们提供了最大限度的确定性让你可以专注于业务本身。踩过坑、补过漏、上过线这个流程走一遍比看十篇教程都管用。希望这一套完整的开发思路能给你的项目提供一些参考。我实际操作下来的感受是踏踏实实把一个全栈闭环做透彻比追求天花乱坠的功能清单有价值得多。后续如果你也想做类似系统可以按照这个思路先跑通一套基础版本再逐步叠加复杂功能步子迈稳一点收获会超过你的预期。

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

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

免费获取报价 →
↑