最近我在整理一套私房菜上门服务系统的完整案例源码,技术栈正是Spring Boot + Vue这套经典组合。这类系统在中小型业务里特别典型——它不只是一个简单的CRUD,而是把“预约服务、在线点餐、订单履约、后台管理”整条链路都串起来了。很多同学拿到类似的源码案例时,最头疼的不是跑不起来,而是看不懂整体设计,也不知道从哪儿下手改。这篇就把我完整跑通这套源码之后的理解和踩过的坑整理出来,从系统设计、数据库表结构、后端接口、前端页面到本地部署,尽量把每一步“为什么这么做”讲透,给正在做毕业设计或者想练手的开发者一个可以直接参考的复现方案。
这套私房菜上门服务系统的核心场景其实很好理解:用户在线浏览私房菜菜品和厨师,下单后可以预约厨师上门做菜,也可以选择让商家配送成品;厨师/商家端负责接单、制作、安排配送;管理员在后台管理菜品、厨师、订单和用户数据。整体听起来像电商,实际上比标准电商多了一层“上门服务”的时间与人力调度逻辑,在这个基础上理解代码就会顺很多。
1. 项目整体设计与技术选型背后的考虑
1.1 为什么Spring Boot + Vue是这类系统的稳妥选择
先聊技术栈。私房菜上门系统属于典型的“管理信息系统 + 轻度O2O”业务,核心诉求是快速交付、逻辑清晰、容易扩展。Spring Boot负责后端接口,Vue负责前端页面,两者前后端分离,中间通过JSON交互——这套方案在中小型项目里几乎是事实标准。
选Spring Boot而不是传统SSH或纯Servlet,最大的原因是“约定大于配置”。一套系统动辄几十张表、几十个接口,如果还像早期那样堆XML配置,光配数据源和事务就能劝退一批人。Spring Boot内嵌Tomcat,打好包一个java -jar就能跑,部署成本极低。配套的Spring MVC、MyBatis-Plus、Spring Security/JWT基本是现成的,社区资料极其丰富,遇到问题搜一下就有答案。
Vue这边,案例里用的是Vue2 + Element UI的组合(也有新项目开始上Vue3 + Element Plus,但Vue2在源码案例里还是主流)。Vue2的API简洁,指令系统清晰,配合Element UI的表格、表单、弹窗组件,做后台管理页面效率非常高。对于私房菜这类交互不算极端复杂的前端,Vue2完全够用。
提示:如果你拿到的源码案例是Vue2,建议先不要急着非要升级到Vue3。Vue2的生命周期、事件绑定、路由守卫等知识迁移性很强,先跑通主流程,之后需要再升级,别在第一阶段引入额外变量。
1.2 系统角色与核心业务流程拆解
一套系统的灵魂在角色和流程,拿到了源码第一件事不是看代码,而是把角色画出来。这套源码里剥离出来有这么几类角色:
- 用户(C端):注册登录、浏览菜品/厨师、加购物车、下单、选择上门或配送、支付(多模拟)、评价。
- 私房菜主/厨师(B端):菜品管理、接单、订单状态流转、查看收入相关数据。
- 管理员(后台):用户管理、厨师审核、菜品上下架、全局订单管理、数据统计。
核心业务流程上有两条主链路,整个代码结构都是绕着这两条线展开的:
- 点餐配送链路:用户浏览菜品 → 加购物车 → 提交订单 → 支付 → 商家接单 → 制作 → 配送 → 用户确认收货 → 评价。
- 预约上门链路:用户选厨师/套餐 → 选上门时间 → 提交预约单 → 厨师接单 → 按约上门 → 服务完成 → 评价。
这两条链路有一个交集点:订单表。理解订单表的状态流转,就等于抓住了整个系统的七寸。后续看后端Service层时,优先看订单相关的几个方法,其他模块基本都是围绕订单在补充数据。
2. 数据库表结构设计与核心字段解析
数据库设计决定了系统的扩展上限。这套源码的表设计走的是行业里比较标准的电商+服务融合模式,核心几张表拆开来讲。
2.1 用户与地址模块
用户表(user)主要字段有:id、username、password、phone、avatar、role、status、create_time。有一个关键点是密码字段,存储的必须是BCrypt加密后的密文,而不是明文。源码里用了Spring Security的BCryptPasswordEncoder,注册时加密、登录时比对,这个习惯建议保留。明文密码在项目里等于裸奔,一旦数据库泄露就是安全事故。
地址表(address)是用户多次下单的基础数据,字段包括id、user_id、contact_name、phone、province、city、detail、is_default。设计上有个值得借鉴的细节:下单时把地址信息快照进订单表,而不是订单表外键关联地址表。为什么?因为用户可能之后改地址,如果订单实时关联,历史订单里的收货地址也会跟着变,这在业务上是不可接受的。快照是一种很常见的“历史数据留痕”做法,做电商、外卖类系统都要有这个意识。
2.2 菜品与厨师模块
菜品表(dish)字段包括id、name、category_id、price、image、description、sales、status。price字段用DECIMAL(10,2),不是float也不是double,这一点我在无数项目里强调过——浮点数算金额会有精度问题,0.1 + 0.2不等于0.3在计算机里是常态,钱的事情上绝不能省心。status字段控制上架/下架,下架的菜品前端不展示,但历史订单里依然保留快照数据。
厨师表(chef)是私房菜上门服务区别于普通外卖系统的核心表。字段包括id、name、avatar、title、skill、base_price、status、sort。这里的base_price可以理解为出诊费或者起步价,后续做预约上门的计价逻辑都要基于它。私房菜场景里厨师是供给方的核心资产,所以后台管理必须有厨师的上下架和排序功能。
2.3 订单模块与状态机设计
订单表(orders)是整套系统数据关系中最核心的一张表,关键字段有:order_no、user_id、chef_id、address_snapshot、total_amount、status、remark、create_time、pay_time、finish_time。
order_no是业务订单号,生成规则常见的是时间戳 + 随机数或自增序列,源码里用了类似yyyyMMddHHmmss + 多位随机数的模式,保证同一秒内不重复。实际生产环境订单号还要考虑并发,但让学生项目或中小型系统用这个规则已经完全够用。
订单状态是这套系统里最有看点的设计,通常用一个整数枚举表示:
| 状态值 | 状态含义 | 触发动作 |
|---|---|---|
| 0 | 待支付 | 下单后创建,超时未支付可取消 |
| 1 | 已支付(待接单) | 用户完成支付,商家端待处理 |
| 2 | 制作中/已接单 | 商家接单,开始制作 |
| 3 | 配送中/已完成上门 | 骑手取餐配送,或厨师上门服务中 |
| 4 | 已完成 | 用户确认收货/服务完成 |
| 5 | 已取消 | 用户主动取消或超时系统关闭 |
客户端只看到文案,后端存储数字,这个设计的好处是数据库占用小、判断方便,但代码里必须写清楚每个状态从哪来、能到哪去。源码里用了状态流转的判断逻辑,比如已取消的订单不能变成已完成,配送中的订单不能直接跳到待支付,这类非法流转在Service层就拦掉了,这个思路值得学习。
订单明细表(order_item)把下单时的菜品名称、单价、数量再存一份快照。为什么要存?菜品价格会变,如果只存菜品ID,将来菜品价格改了,历史订单的对账就是一笔糊涂账。订单和明细是一对多关系,用订单ID关联,下单时把当前菜品表的数据“复制”到明细表,这就是快照的实际落地。
3. 后端Spring Boot核心实现解析
3.1 项目分层与代码结构
拿到源码之后,先看包结构,这套系统的后端分层非常标准:
- controller:接收请求、参数校验、返回结果
- service:业务逻辑,事务边界在这里
- mapper:数据库操作,基于MyBatis-Plus
- entity:数据库实体
- dto/vo:入参出参封装
- config:配置类(跨域、拦截器、静态资源映射)
- util:通用工具类
分层的好处是职责清晰,改一处不动全局。比如菜品价格展示需要加一个折扣字段,只需要改VO和Service层的组装逻辑,Controller基本不用动。新手容易犯的错是把业务逻辑全写在Controller里,看起来爽,后面维护就是灾难。案例源码这点做得不错,我是建议照着它的分层习惯来,不要自己另起炉灶。
3.2 登录鉴权与JWT设计
私房菜系统里有三种角色(用户、厨师商家、管理员),所以登录鉴权必须能识别角色。源码用的是JWT方案:用户登录成功后,后端签发一个token,把用户ID和角色信息放到token里,前端存储到localStorage,请求时通过axios拦截器放到Header的Authorization字段。
后端有一个拦截器(Interceptor),每次请求进来先验证token的合法性和有效期,再根据接口要求的角色做权限判断。比如管理员接口只有admin角色能访问,普通用户访问直接返回401/403。这里有一个实操细节:拦截器里要放行登录接口、注册接口、菜品浏览接口这些不需要鉴权的请求,否则前端没登录时连首页都打不开,这是新手最常踩的坑。
我建议你把token的有效期拆成两个概念看:token本身的过期时间(exp)和用户被踢出的时机。中小型系统不需要搞refresh_token那一套复杂逻辑,过期了就让用户重新登录,体验上能接受,实现上也简单可靠。
3.3 下单核心流程与事务控制
下单是整套系统里最“值钱”的代码,源码里下单流程大致是:
- 从购物车查出用户勾选的菜品列表。
- 遍历菜品判断是否上架、库存是否充足。
- 计算总金额(用BigDecimal逐项累加)。
- 生成订单号和订单明细快照。
- 扣减菜品销量/库存。
- 清空购物车中已下单的条目。
- 返回订单ID,跳转支付。
这里最关键的是加 @Transactional 注解,保证“扣库存+写订单+清购物车”是一个原子操作。任何一个环节抛异常,整个事务回滚,绝不会出现钱付了库存没扣、或者库存扣了订单却没生成这种状态错乱问题。
注意:事务不是加了注解就万无一失。事务方法不能在同类里通过this调用,否则注解失效——Spring事务是基于代理的,this调用不会走代理。所以下单方法必须由Controller调用Service的公开方法,不要在Service内部互相调。
支付环节通常是一个模拟接口,因为这个系统本身没有接真实支付渠道。模拟支付就是接收一个订单ID,把订单状态从“待支付”改成“已支付”。接真实支付时,要把“回调通知”这个异步过程接进来,回调里验签、改状态、处理并发,那是另一个复杂度级别的活,这里不展开。
3.4 文件上传与图片访问
菜品和厨师的图片上传,源码里用的是本地存储方案:前端传multipart文件流,后端接收后把文件写到配置的上传目录,再把文件路径存数据库。访问图片时通过配置类里的静态资源映射,把“/images/**”这类URL映射到本地磁盘目录。
这个方案在中小型项目里够用且简单,不需要对象存储服务。但有两个坑必须提前避开:
- 上传目录不要放在项目打包后的临时目录里,不然重启项目图片就丢了。最好放到一个固定的磁盘路径,比如linux下的/data/upload,Windows下的D:/upload,然后在配置里引用。
- 跨域访问图片时,如果前端和后端域名不一致,静态资源映射要配合跨域配置一起生效,否则图片会加载不出来。
我在跑这套源码时遇到过一次图片404,排查到最后就是静态资源映射路径和实际存储路径没对上。这类问题高发,建议你在部署时就确认好配置项,别等页面图片全裂了再回头查。
4. 前端Vue实现与页面交互拆解
4.1 前端工程结构与路由权限
前端技术栈是Vue2 + Vue Router + Vuex + Element UI + Axios,这也是案例源码里最常见的一套组合。src目录下大概的结构是api、router、store、views、components、utils。views目录按业务拆页面:首页、菜品列表、菜品详情、购物车、订单确认、订单列表、后台管理的各种页面。
前端路由权限用了路由守卫(beforeEach)加meta标记的方式:需要登录才能访问的页面meta里加requiresAuth: true,需要管理员身份的页面加role: 'admin'。每次路由跳转前,守卫里检查有没有token,没有就跳登录页;有token但角色不够就跳首页,顺带提示无权限。这个方案不复杂,但能解决“用户直接改URL越权访问后台”的常见问题。
4.2 Axios封装与接口对接
前端所有请求都是通过一个统一的request.js发起,源码里做了标准封装:请求拦截器里带上token,响应拦截器里统一处理HTTP状态码和业务状态码。这里有几个细节值得注意:
- 后端返回的统一结构一般是{code: 200, msg: '成功', data: {...}},前端拦截器先判断code,非200统一弹错误提示。
- 401表示token过期,响应拦截器里要清掉本地token并跳转登录页,否则用户会一直卡在页面里点啥都没反应。
- 文件上传要单独设置Content-Type为multipart/form-data,普通的JSON封装是不带文件上传能力的,源码里单独封装了一个upload方法。
4.3 购物车与下单页面的状态联动
购物车数据存在Vuex里,同时同步一份到localStorage,这么做是为了刷新页面不丢数据。加到购物车是前端本地行为,不经过后端,只有下单时才把购物车数据提交给后端。这个设计很务实,购物车本来就是临时性的,没必要每次都调接口。
下单确认页面会把购物车选中的菜品展示出来,同时让用户选择配送方式(普通配送/预约厨师上门)、填地址、填备注。页面提交时把购物车数据+地址ID+配送方式一起提交到后端下单接口。前端跳转的先后顺序是:提交成功 → 生成订单 → 跳转支付页 → 模拟支付成功 → 跳转订单列表。
有一个前端细节很容易忽略:用户从购物车页面进入下单页时,如果购物车是空的,应该直接拦截跳回菜品列表,不然会出现提交空订单的情况。源码里在这个位置做了判断,这种边界情况恰恰是答辩时最容易被问到的点。
4.4 后台管理的表格与表单实践
后台管理页面(菜品管理、厨师管理、订单管理、用户管理)基本是Element UI的el-table + el-form + el-dialog组合。源码里封装了通用分页组件,翻页、搜索、重置都走同一个逻辑。表格里的操作列有编辑、上下架、删除等按钮,通常用el-dropdown或直接放多个按钮。
这一块页面量大但套路统一,对着源码改字段就能做出新页面。比如要给菜品加一个“是否推荐”的标记,前端加一列switch,后端实体加一个字段,Mapper不用改(MyBatis-Plus自动映射),Service和Controller照葫芦画瓢,半小时就能搞定。这也是我为什么说这套源码对新手做毕设练手很友好——框架已经搭好了,往里填业务就行。
5. 本地部署与全流程运行指南
5.1 环境准备与版本搭配
跑这套源码,环境版本搭对了,基本不会卡在“起不来”这一步。我实测下来的推荐组合是:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | Spring Boot 2.x最佳搭档,稳定省心 |
| Maven | 3.6+ | 管理后端依赖 |
| Node | 14.x或16.x | Vue2项目这个版本区间最稳 |
| npm | 6.x或8.x | 对应上面Node版本 |
| MySQL | 5.7或8.0 | 两个版本都行,字符集统一utf8mb4 |
| Redis | 可选 | 源码如果没强制用缓存,可以先不装 |
版本这块我踩过一次坑:用Node 18跑Vue2老项目,npm install的时候报了一堆依赖兼容错误,换成Node 14后就安静了。如果是Vue2 + Element UI项目遇到依赖装不上的问题,先检查Node版本,多半是它。
5.2 数据库导入与后端配置
后端启动前要做两件事:建库导数据、改配置。
先在MySQL里创建一个数据库(比如叫private_kitchen,具体看源码里SQL脚本的库名),然后把项目里XXX.sql文件导入。SQL文件一般自带建表和数据,导入完成后能看到十几张表以及不少测试数据。这里提醒一句:导入之前用文本编辑器打开SQL看一遍,确认里面的库名和你创建的一致,不一致就改。
后端配置集中在src/main/resources/application.yml里,核心要改的就是数据库连接:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/private_kitchen?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的密码注意url里的serverTimezone=Asia/Shanghai,这个不加的话,数据库连接经常报时区错误。字符集参数characterEncoding=utf8也要带,不然中文直接乱码。
5.3 后端打包启动
在项目根目录执行:
mvn clean package -DskipTests可以跳过测试减少打包时间,打包完成后target目录下会生成一个jar包,然后:
java -jar target/xxx.jar看到类似“Started Application in xxxx seconds”的日志就算启动成功。如果电脑没配Maven环境变量,直接用IDE(比如Idea)里的Maven面板双击package也行。
后端启动后先不要急,用浏览器访问http://localhost:8080/接口路径能出JSON,或者至少确保端口没被弹错,再进入前端步骤。前后端分离项目,联调前先确认后端活着,能省掉一堆排查时间。
5.4 前端依赖安装与开发服务器启动
前端目录下执行:
npm install这个步骤在环境网络不好时可能会卡很久,或者报ERESOLVE依赖解析错误。实在装不上可以尝试:
npm install --legacy-peer-deps这是Vue2老项目常见的救命咒语,原因是对等依赖版本较新Node不认。装完依赖后:
npm run dev默认跑在localhost:8080或9527(看vue.config.js配置)。开发模式下前端会通过proxy代理把/api请求转发到后端8080端口,这样就不会有跨域问题。vue.config.js里通常长这样:
module.exports = { devServer: { port: 9527, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }如果访问前端页面时接口全部404或502,八成是proxy的target端口和后端实际端口不一致,改过来就好。
5.5 生产环境构建与Nginx部署
如果要部署到云服务器,前端需要先构建:
npm run build生成dist目录,然后把dist里的静态文件放到Nginx的html目录,同时用Nginx反向代理后端接口:
server { listen 80; server_name 你的域名或IP; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那行很重要,Vue是单页应用,刷新子路由时Nginx要回退到index.html,否则会出现404。后端jar包在服务器上用一个守护进程工具(比如systemd或进程守护工具)常驻运行,注意别让终端一关服务就停了。
6. 常见问题排查与避坑经验实录
6.1 端口占用导致服务启动失败
后端启动时报“Port 8080 was already in use”,说明端口被占了。暴力且有效的办法:
# Linux / macOS lsof -i:8080 # Windows netstat -ano | findstr 8080查到PID后直接kill掉,或者改application.yml里的server.port换一个端口。前端端口占用同理,改vue.config.js里的port即可。还有一种情况是改了端口后忘改前端proxy的target端口,前端跑起来了但接口全挂,别问我怎么知道的。
6.2 跨域问题表现与解决
前后端分离项目如果不用代理,直接在前端用localhost:9527去请求localhost:8080,浏览器控制台会报CORS错误。解决方案有三条路:
- 本地开发用代理(推荐,改vue.config.js即可)。
- 后端加全局跨域配置(@CrossOrigin或WebMvcConfigurer),适合接口要和多个前端联调的情况。
- 生产环境用Nginx反向代理(最佳实践,前端和后端同域)。
实际项目中我建议把后端跨域配置也加上,反正不影响Nginx方案,而且遇到直接POST调用接口排查时方便得多。
6.3 登录后请求接口返回401
这个现象一般是token没传或传了已过期的token。排查顺序:
- 看登录成功后前端有没有把token保存到localStorage。
- 看axios封装里有没有从localStorage取token并设置到请求头。
- 看后端拦截器有没有放行登录接口本身。
- 看token过期时间是不是太短,源码默认一般2小时,调试时改成24小时省心。
6.4 上传图片后页面显示裂图
裂图原因基本是访问不到图片地址。按这个顺序查:
- 数据库里存的图片路径是什么,是相对路径还是完整路径。
- 后端静态资源映射有没有配置。
- 上传目录在磁盘上是否真实存在,文件夹有没有写权限。
- 前端拼接图片URL的base是不是和后端映射一致。
Linux服务器上经常是目录权限问题,chmod给足权限就能解决。Windows上跑项目反而很少遇到权限问题,但路径分割符要注意,建议配置里用相对路径再统一转换成绝对路径。
6.5 MyBatis-Plus分页查不出数据
源码里如果用了MyBatis-Plus分页插件,要注意有没有配置PaginationInnerInterceptor。这个插件必须在config里显式注册,否则调用page方法时不会真的分页,返回的总条数永远是0或者查全表。这个坑相当隐蔽,因为它不报错,只是数据表现不对。
6.6 中文数据显示乱码
后端返回的中文正常,前端页面显示问号,一般是数据库连接字符集问题。检查application.yml里有没有characterEncoding=utf8,数据库表和字段的排序规则是不是utf8mb4_general_ci,还有前端HTML页面meta标签是不是utf-8。三处都统一,基本不会再乱码。
心得:调这类问题的时候,一定要分开验证接口层数据正不正常(直接用浏览器访问接口URL看返回),再判断是不是前端显示问题。很多人一上来就改前端,折腾半天发现后端返回就已经乱码了。分层排查是最省时间的思路。
7. 源码二次改造与功能扩展建议
7.1 改造前先画清楚主线
这套源码的价值不在于“能跑”,而在于“能改”。改的前提是把业务主线摸清楚——用户从注册到下单再到评价,数据是怎么流转的。我给一个可操作的建议:拿到源码后,用一张A4纸把涉及订单状态变化的所有接口列出来,每个接口标注“谁调用的、改了什么字段、有什么前置条件”。这张纸画完,你对系统的理解就超过大多数只看代码的人了。
7.2 加一个菜品分类管理功能
很多源码自带的菜品分类是写死在前端下拉框里,后端没有分类表。想扩展,可以先加一张category表,然后在菜品表加category_id字段,后端菜品接口返回时关联分类名,前端菜品列表按分类维度做筛选。这个功能涵盖建表、实体、Mapper、Service、Controller、前端下拉选联动,一整套流程走下来,基本就把这套架构玩明白了。
7.3 把模拟支付换成真实支付
源码里的支付是模拟的,点个按钮就改状态。要接真实支付(比如常见第三方支付),核心是处理好回调:用户支付成功后支付平台会异步通知你的后端,后端收到通知先验签,再根据订单号把订单状态改成已支付。这里要特别注意幂等性,防止回调通知多次导致重复处理。
7.4 Docker化部署
源码默认是jar包+前端静态文件部署,想省事可以写一个简单的Dockerfile,后端一个容器、前端一个容器、MySQL一个容器,用docker-compose编排。我在实际部署中体会是,Docker化的好处不只是方便迁移,更重要的是把环境差异彻底消灭掉,今天在开发机跑通,明天到另一台服务器照样能跑,不用再重复踩环境配置的坑。
这套私房菜上门服务系统的源码我从底层翻到前端,整体结构清晰,注释还算到位,核心的订单状态流转和权限设计都符合中小型项目的标准做法。如果你正在选毕设题目,或者想找一个能快速上手的前后端分离案例练手,这套源码的复用价值是比较高的。个人建议拿到之后先完整跑通,再照着业务主链路改一个自己熟悉的小场景,比如把私房菜换成家政服务或美容预约,思路完全是通的。动手改几轮,比看十篇零零散散的文章收获大得多。