私房菜上门服务系统实战:SpringBoot+Vue前后端分离全栈开发

发布时间:2026/10/12 8:42:30
私房菜上门服务系统实战:SpringBoot+Vue前后端分离全栈开发 这个项目我前前后后改了三轮第一次是照着课程作业的思路套模板第二版才真正理解“定制上门”这个业务的本质最终版连数据库都重构了一遍。今天把这个前后端分离的私房菜定制上门服务系统完整拆开讲从表结构、权限模型、订单状态机到部署上线每个环节的关键取舍和踩坑记录都写在下面。这套系统本身的技术栈非常主流SpringBoot 做后端接口Vue 做前端页面MyBatis 负责数据库操作MySQL 存数据。业务场景却很垂直——用户在线浏览私房菜菜单按口味偏好和上门时间下单厨师接单后上门服务完成后双方互评。类似的流程搬到宠物美容、家政保洁、上门维修几乎都能直接套用所以不管你是想拿来学习前后端分离的实战项目还是准备作为毕设或商业项目的基础模板这篇文章都能帮你少走不少弯路。1. 需求分析与整体设计1.1 业务痛点与功能拆解做私房菜上门商家最头疼的不是做菜而是管单。我认识一个做私房菜的朋友早期全靠微信接单客户问“今天有没有空档”“能不能不放香菜”“几点能到”他得一直翻聊天记录。刚开始单少还能扛后面一天二十多单光是回复“今天满啦”就要花两个小时。这套系统要解决的就是把上面这些“人肉流程”变成规范化操作。用户端注册登录、浏览私房菜菜单、按分类筛选、线上下单填口味偏好、预约上门时间、地址、查看个人订单、服务完成后评价。厨师端查看指派给自己的订单执行接单、开始制作、完成服务这些状态流转操作。管理后台菜品上下架、订单状态人工干预、用户管理、评价审核。功能名字看着都很常规但真正落到数据层面核心不是“增删改查”那几下而是订单状态如何流转。用户提交订单后是“待接单”厨师接单变“制作中”上门做完变“服务完成”用户评价后整个流程闭环。任何一步状态跳变都必须由对应用户触发不能让用户自己把已取消的订单改成已完成。需求层面还有一个细节特别值得注意口味偏好和上门时间不是可选项而是订单的核心字段。普通电商订单只需要商品ID、数量、收货地址但私房菜订单必须有备注、忌口、期望时间否则厨师上门根本没法准备。数据库表设计的时候这些字段就必须进主订单表而不是塞在“其他信息”里。1.2 技术选型为什么是 SpringBoot Vue MyBatis MySQL选型的时候我没有刻意追新框架原因很现实这个系统要的是成熟稳定、好排错、资料多。SpringBoot 的自动配置省掉一堆 XMLVue 做单页应用前后端分离后开发体验和部署体验都舒服MyBatis 手写 SQL多表联查的语义一目了然MySQL 不用多说小中型业务量的绝对主流。前后端分离到底带来什么好处说个实际场景如果某天产品说要上小程序后端接口完全可以复用只需要新写一个小程序前端反过来如果只想调整订单列表的展示交互前端改完重新构建部署静态页面就完事后端完全不参与。这就是职责分离的长期价值。这套技术栈的具体职责划分层级技术方案职责说明前端框架Vue 2/3 Vue Router Pinia/Vuex页面渲染、路由控制、全局状态管理UI 组件Element UI / 自定义组件表格、表单、弹窗、消息提示接口通信Axios 统一封装请求自动携带 token、拦截 HTTP 错误统一提示后端框架SpringBoot 2.x/3.x提供 RESTful API、参数校验、事务管理持久层MyBatis MySQL手写 SQL、动态条件查询、多表关联鉴权方案JWTJSON Web Token无状态登录识别、角色权限控制这套组合最大的优势是坑少、案例多。不管你是第一次做还是准备二次开发碰到的绝大多数问题都能找到现成的解决方案。1.3 数据库设计5张核心表撑起整个业务数据库是系统的地基我设计的时候把核心表控制在几张以内不要一开始就铺一大堆冗余表和中间表。必备的表有这些user用户表id、username、password、phone、roleUSER/CHEF/ADMIN 三种角色、status。密码必须加密存储不能存明文。dish菜品表id、name、category_id、price、description、image、status上/下架。orders订单表id、order_no、user_id、address、appointment_time、remark口味偏好和忌口、status、create_time。建议加一个 order_no 业务订单号用户沟通时常用订单号而不是自增 id。order_dish订单菜品关联表id、order_id、dish_id、quantity。因为一个订单可能包含多道菜必须用关联表不能把菜品 id 塞在一个字段里。evaluation评价表id、order_id、user_id、content、rating1~5、create_time。订单菜品关系这张表我要重点说一下。有两种设计方案一种是把菜品 ID 和数量用逗号或 JSON 拼成一个字段简单粗暴另一种是搞一个独立关联表。我选了关联表。原因后续要统计“哪道菜卖得最多”“哪道菜评价最好”关联表一条 SQL 就能查出来JSON 方案取出来还得解析统计维度一多代码就烂了。如果只是为了演示能跑JSON 也行但项目想往后走关联表是必须的。建表时有两个坑非常高频字符集必须用 utf8mb4不是 utf8。用户评价里放一个 emoji 表情utf8 直接报错“Incorrect string value”。时间字段用datetime而不是varchar。前期看着没啥区别后期做时间排序、统计报表时就明白了字符串比较时间早晚早晚会出错。2. 后端核心实现细节2.1 分层架构与目录组织后端用的是 SpringBoot 最常见的四层结构不是花哨的 DDD就是务实的分层com.example.chefservice ├── controller // 接口层收参数、调 service、返回结果 ├── service // 业务层事务控制、状态流转、业务校验 ├── mapper // MyBatis 接口 XML ├── entity // 数据库实体类 ├── common // 公共类统一返回结果、状态枚举、异常处理 └── config // WebMvc 配置、CORS 配置、拦截器注册四层结构的意义在于代码出了问题你第一时间就知道去哪找。Controller 层不要写任何业务逻辑只负责参数接收和数据响应。业务判断全部下沉到 Service事务控制也在 Service 层打注解。这样 Controller 看起来干干净净排查问题时就少翻一半代码。实体类我建议直接用 Lombok 的Data少写大量 getter/setter。很多教程爱手写但真实项目里没人那么干。2.2 JWT 鉴权与角色权限控制登录接口本身没什么神秘的校验用户名密码通过后生成一个 JWT 字符串返回给前端。关键在拦截器public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录); } Long userId JwtUtil.parseToken(token); request.setAttribute(userId, userId); return true; } }拦截器在 WebMvcConfigurer 里注册注意放行登录、注册和菜品列表这些不需要鉴权的接口registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/dish/list);角色权限怎么控制我没有引入 Spring Security因为权限点不多用拦截器加自定义注解的方式足够Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String value(); }在 Controller 方法上加RequireRole(ADMIN)拦截器里读取注解判断角色。这套方案代码量小、直观新手也容易理解。Spring Security 功能强大但学习曲线陡对这种规模的项目属于杀鸡用牛刀。实操心得JWT 密钥绝对不能写死在前端代码或日志里正式部署时配置到环境变量中并且定期轮换。另外 JWT 有个固有问题——用户被封禁后 token 还能继续用。如果系统有拉黑用户的需求要么在拦截器里查一下用户状态要么引入 Redis 黑名单。小项目每次查一下库逻辑可控成本也不高。2.3 订单模块状态机 事务管理订单模块是整个系统的“心脏”这里的业务规则最密集。状态定义用枚举public enum OrderStatus { PENDING(0, 待接单), ACCEPTED(1, 已接单), COOKING(2, 制作中), COMPLETED(3, 服务完成), CANCELED(4, 已取消); }每个状态流转接口都要校验当前状态是否合法。比如“服务完成”只能从“制作中”流转用户不能把“待接单”直接改成“已完成”。状态机的价值就在这把状态之间的合法路径固化在代码里业务就不可能乱。创建订单是典型的多表事务场景Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { Orders order new Orders(); // ... 组装订单数据 orderMapper.insert(order); // 批量插入订单菜品关联 for (OrderDish item : dto.getDishes()) { orderDishMapper.insert(item); } return order.getId(); }这里有一个必须提醒的点Transactional默认只在遇到运行时异常时回滚。如果业务代码里 catch 住了异常但没有往外抛事务不会回滚。我在这上面吃过一次亏订单明细插入失败订单头已经插进去了数据对不上排查了半天才意识到是事务没生效。事务边界的原则是只把需要原子性的操作放进事务。生成订单加扣减库存放一起没问题但“发送短信通知”这种外部调用就不要放事务里否则短信服务超时会导致整个订单创建回滚用户体验极差。2.4 MyBatis 动态 SQL 与联表查询手写 SQL 是 MyBatis 的最大优势查询效率心里有数动态条件也不怕。两个典型例子。菜品分页条件查询select idselectDishPage resultTypecom.example.chefservice.entity.Dish SELECT * FROM dish where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /select订单详情联查用户 菜品 评价关联select idselectOrderDetail resultMapOrderDetailMap SELECT o.*, u.username, u.phone, d.name AS dish_name, d.price AS dish_price FROM orders o LEFT JOIN user u ON o.user_id u.id LEFT JOIN order_dish od ON od.order_id o.id LEFT JOIN dish d ON od.dish_id d.id WHERE o.id #{orderId} /select使用 MyBatis 有两个细节值得注意一是if判空时注意数字类型Long类型判空直接检查 null 即可不需要加空字符串判断不然某些情况下会类型转换异常二是表名尽量别叫order因为 order 是 MySQL 保留字每次写 SQL 都要反引号包起来太别扭。我表名用的orders这个细节能帮你省掉无数次莫名其妙的 SQL 报错。常见问题联表查询时两个表都有id字段如果不指定别名MyBatis 后面的字段会把前面的覆盖掉导致查询结果里 id 全部一样。解决办法是用别名或者用 resultMap 做字段映射。这个坑我帮别人排查过很多次了。3. 前端核心实现细节3.1 页面规划与路由设计前端整体是 SPA 单页应用页面分为四大块前台页面首页、菜品列表、菜品详情、下单页、订单列表、评价弹窗。用户中心个人资料、我的订单、评价记录。厨师端页面我的服务订单、接单/完成操作。后台管理页面菜品管理、订单管理、用户管理。路由大致如下const routes [ { path: /, component: Home }, { path: /dish, component: DishList }, { path: /orders, component: OrderList, meta: { requiresAuth: true } }, { path: /admin, component: AdminDashboard, meta: { requiresAuth: true, role: ADMIN } } ]路由守卫统一控制权限router.beforeEach((to, from, next) { const role store.state.role; if (to.meta.requiresAuth !role) return next(/login); if (to.meta.role to.meta.role ! role) return next(/); next(); });千万别在每一个页面里单独判断权限代码冗余还容易漏。集中式守卫统一判断一次后续维护时只改一处就够。按钮级权限比如“确认接单”只有厨师角色可见在组件里用v-ifrole CHEF控制即可简单直接。3.2 Axios 封装与跨域处理前端所有接口请求必须走同一个 Axios 实例不要到处axios.get裸调。封装里做三件事请求拦截器从 localStorage 取 token自动放进请求头。响应拦截器返回结果里 code 为 200 就返回数据否则弹出错误提示。统一处理 401登录过期时跳回登录页并清除本地信息。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; if (res.code 200) return res; if (res.code 401) { localStorage.removeItem(token); router.push(/login); } Message.error(res.message || 系统异常); return Promise.reject(new Error(res.message)); }, error { Message.error(网络异常); return Promise.reject(error); } );开发环境的跨域通过 Vite 或 Webpack 的 proxy 解决server: { proxy: { /api: http://localhost:8080 } }生产环境的跨域用 Nginx 反向代理配置。这里必须强调一个容易错的事代理转发时路径要对齐。后端接口统一以/api开头前端请求也统一以/api开头Nginx 配proxy_pass的时候路径不会乱。前后端不一致是最常见的联调事故源头之一。3.3 下单与评价的交互闭环下单流程前端是这样的菜品列表点击“下单”进入详情页选择数量、填写口味偏好忌口、辣度、上门时间。提交订单后端创建订单成功返回订单号。跳转订单列表页显示“待接单”。厨师端看到订单点击“接单”状态变“已接单”。厨师上门服务完成后状态变“服务完成”用户看到评价按钮。用户填写评分和文字评价订单流程闭环。评价表单有一个设计细节评分用 1~5 星的滑块组件提交之前校验评分不能为空文字评价可选但建议填。后端保存评价之后订单状态本身不需要再加“已评价”这个枚举值因为评价表里存在该订单的记录就已经说明评价完成了。查询订单列表时联查评价表判断一下即可省了一个状态字段状态机也更简洁。前端表单校验不能漏上门时间必须晚于当前时间、地址长度不能太短、备注不能超过 200 字。这些规则在后端接口同样要校验不能只信前端传过来的值别人用 Postman 直接调接口分分钟绕过你的前端校验。4. 部署实操与常见问题排查4.1 后端打包与环境准备后端部署用最简单朴素的java -jar方式先保证能跑起来再考虑容器化。部署前确认环境软件版本建议说明JDK1.8 或 11与开发环境保持一致避免编译版本问题MySQL5.7 或 8.0推荐 8.0字符集和性能都更好Maven3.6打包用Nginx1.18静态资源服务 反向代理打包命令很简单mvn clean package -DskipTests java -jar target/chef-service-0.0.1-SNAPSHOT.jar启动前必须修改配置文件里的数据库连接、密码和端口不要用开发环境的默认值。还有一个特别坑的细节MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver连接串要追加serverTimezoneAsia/Shanghai。不设置时区的话数据库里的时间字段会差 8 个小时排查起来完全没有头绪。启动后验证访问http://服务器IP:8080/api/dish/list能返回 JSON 数据就说明后端起来了。如果进程卡住或者立刻退出去 target 目录下看日志文件重点看数据库连接异常和端口占用。4.2 前端构建与 Nginx 部署前端构建npm install npm run build构建完成后dist目录是纯静态文件上传到服务器目录比如/var/www/chef。Nginx 配置如下server { listen 80; server_name your-domain.com; root /var/www/chef; index index.html; location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }三个要点try_files $uri $uri/ /index.html;这行必须写否则前端路由直接刷新/orders这种子页面会返回 404。SPA 部署这是最经典的坑。后端换端口或路径只改 Nginx 的proxy_pass前端代码不用重新构建。部署 HTTPS 时申请证书配置 SSL还要处理 HTTP 跳 HTTPS 规则不然用户访问 http 地址会报不安全。4.3 踩坑实录从开发机搬到服务器后常见的报错部署阶段最容易踩的坑我逐个列出来命中率极高第一个是数据库连接失败。排查顺序MySQL 服务启动没有、端口 3306 放行没有、账号密码对不对、驱动类名对不对、数据库表导入没有。很多时候不是密码错了而是防火墙没放行 3306或者 MySQL 只绑定了 127.0.0.1 不接受远程连接。第二个是跨域失效。开发环境有 proxy 一切正常一到服务器就报跨域。最常见原因是 Nginx 没配反向代理或proxy_pass少了路径导致请求 404。开发环境不要开跨域插件骗自己生产环境的坑迟早要还。第三个是前端刷新 404。上面说过配置try_files解决。第四个是中文乱码。服务器上后端日志看到中文乱码一般是文件编码或系统默认编码问题设置启动参数-Dfile.encodingUTF-8可以解决大部分情况。最后分享一个提高排错效率的习惯不要在没有任何日志输出的情况下靠猜。开发环境把 MyBatis 的 SQL 日志打开mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplSQL 直接打印到控制台字段名写错一眼就能看到。生产环境再关掉这个配置避免日志刷屏。我第一次部署完发现订单创建总是失败就是靠这行配置定位到 SQL 里写错了一个字段名。5. 扩展思路与个人体会5.1 这个系统的扩展方向做完整套系统之后我梳理了几个明显的扩展方向消息通知订单关键节点接入短信或站内信。比如厨师接单后自动发消息给用户需要考虑引入消息队列或第三方短信服务。数据可视化管理后台加一个统计面板展示每日订单数、销售额、菜品销量排行、评价平均分。前端用 ECharts后端提供聚合统计接口。多门店支持如果做成品牌连锁订单表要增加门店字段厨师与门店关联权限模型更复杂。接口文档化引入 Knife4j 或 Swagger前后端协作时接口文档自动生成不用再找人来问“这个接口要传什么参数”。5.2 做垂直业务系统的通用经验做完这个私房菜系统我最大的体会是技术上真正难的地方不多最花精力的是业务规则和状态流转的设计。这个项目里的表结构、权限模型、订单状态机几乎可以平移到家政预约、上门维修、宠物美容等其他垂直服务场景改动量主要是业务表字段和页面文案。还有一个反复被验证的经验不要在联调阶段临时改数据库字段类型尤其是订单表。后面改字段代码、缓存、历史数据、报表全部受影响。前期把字段语义定义清楚比后期优化一万次都重要。任何项目前期设计多花一小时后期开发和排障能省一整天。最后再分享一个选型层面的心得技术栈不要盲目追求花哨。周围有朋友用微服务架构做过类似系统结果一个订单查询跨了三个服务部署调了半天纯粹是自己给自己找麻烦。像这种垂直领域的小系统SpringBoot Vue MySQL 就是最优解等业务量真正上来了再考虑拆分也不迟。系统的价值永远在于解决业务问题而不是技术炫技。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询