拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Boot + Vue 共享厨师预约平台:全栈设计与实践指南

1. 项目概述1.1 项目背景与需求来源共享厨师预约平台本质上解决的是一个典型的撮合问题。用户想吃家宴、私房菜不想去餐厅排队也不一定愿意天天自己下厨厨师有手艺、有空闲时间但缺少一个通道把服务能力卖出去。这个平台就是把这两端的人拉到一起用户在线选厨师、约时间、下单支付厨师接单、上门或到店服务然后再完成评价和结算的闭环。这类项目在当前的互联网环境下有很强的现实需求尤其是私厨、家宴、月子餐、老年膳食等细分场景不断扩大。从技术角度看它又是一个非常典型的全栈Web项目后端用Spring Boot构建RESTful API前端用Vue搭建单页应用数据库层做业务建模再加上支付、短信通知、评价体系等周边能力。拿这个题目练手或者作为毕业设计既能覆盖主流技术栈又能把业务逻辑做深性价比很高。下面我按照实际做这个项目的思路从整体设计、技术选型、数据库建模、代码实现、常见坑点几个维度完整拆解一遍。无论你是要复现这个项目还是在此基础上做二次开发都希望能少走弯路。1.2 项目需要解决的核心问题做系统之前先得把问题域理清楚。什么是共享厨师预约平台最核心的问题我总结下来是这四件事用户和厨师两端的信息不对称需要一个公开透明的展示与搜索机制。预约过程涉及时间、地点、服务内容、费用等多个维度必须有一套可靠的状态流转机制。支付和结算天然涉及资金安全订单状态与资金数据必须严格一致。厨师的技能资质、历史评价、服务记录需要沉淀形成可信任的服务档案。这四点分别对应着系统的功能模块、数据库设计、接口幂等性和评价体系。整个项目能不能撑起来就看这四块做得到不到位。2. 核心业务分析与整体设计思路2.1 角色梳理与权限模型一个预约平台角色不可能只有用户和厨师两个。实际做下来至少需要四类角色普通用户C端下单人、厨师服务提供者、平台管理员审核与运营、系统超级管理员技术运维。四类角色的权限边界必须一开始就划清楚不然后面改起来非常痛苦。我采用的方案是基于RBAC基于角色的访问控制模型后端用Spring Security JWT做认证授权前端用Vue Router的全局前置守卫做路由级控制。给每个角色分配不同的菜单和接口权限用户只能看到浏览、下单、评价等功能厨师只能看到接单、排期、收益等功能管理员能看到审核、订单管理、数据分析等功能。权限这块不要想着做得简单因为一旦上线运营权限漏洞往往是第一个出事的。2.2 功能模块拆分按业务域来划分这个项目大致可以拆成以下六大模块模块核心功能说明用户模块注册、登录、个人信息、地址管理支持手机号验证码登录第三方登录可扩展厨师模块厨师入驻、资质审核、排期管理、菜系标签厨师主页展示拿手菜、评分、接单量预约模块按条件搜索厨师、选择时间、提交预约、支付定金状态机流转防止时间冲突订单模块订单查询、取消、退款、完成确认涉及支付回调、超时处理评价模块服务完成后的评分与文字评价驱动平台信任体系的关键管理后台厨师审核、用户管理、订单干预、数据统计前后端分离后单独部署的管理端这个拆分不是我凭空想的而是在做过同类项目后总结出来的结构。严谨地说这样的模块划分遵循了单一职责原则——每个模块的职责清晰开发时可以多个模块并行后期维护也只是改局部而非动全局。2.3 为什么选择前后端分离架构说到Spring Boot Vue这个组合它最核心的价值是前后端分离。前后端分离的本质是前端和后端各自独立开发、独立部署、独立演进通过标准化的HTTP接口进行通信。这样做有几个实际的好处第一开发和调试不再互相阻塞。前端工程师用Mock数据先跑起来后端工程师专心抠接口两边同时在推进项目周期能明显压缩。第二前端静态资源和后端业务逻辑可以分开部署前端扔到Nginx上后端打jar包运行一旦遇到高并发后端可以水平扩容前端可以做CDN加速。第三Vue这种组件化框架和RESTful API的结合非常顺畅数据的单向流动让状态管理变得可控。不过前后端分离也有它的代价最典型的就是跨域问题和联调成本。跨域后面我会细讲踩坑经历联调这块建议从项目一开始就约定好接口文档规范Swagger/OpenAPI别等到前后端都写完再对那时候就等着哭吧。3. 技术选型与开发环境搭建3.1 后端技术栈清单Spring Boot是这个项目的骨架我建议直接上2.7.x或者3.x的稳定版本。如果只做毕设或者练手2.7.x最保守如果是新项目且对Java 17熟悉可以直接用3.x。配套的技术栈我列一下Spring Boot核心框架IoC容器、自动配置、依赖管理Spring Security JWT认证鉴权无状态登录设计方案Spring Data JPA 或 MyBatis-Plus持久层框架两者选一MySQL主数据库存储业务数据Redis缓存用户会话、验证码、热点数据Maven依赖管理和构建工具这里要特别说一下持久层框架的选择。Spring Data JPA适合实体关系复杂的项目写起来快MyBatis-Plus适合SQL偏好明确、需要复杂查询的团队。我的建议是如果你更熟悉SQL直接用MyBatis-Plus因为它的代码生成器和分页插件在开发效率上优势很大如果你希望尽量减少SQL写法JPA也不差。关键别两边混用那才是灾难。3.2 前端技术栈与工程化配置前端我采用的是Vue 3 Vite Pinia Element Plus这套组合。相比Vue 2的Webpack体系Vite的开发服务器启动速度优势非常明显——跑大项目的时候几乎是秒开热更新也快调试体验好很多。工程化方面有几个关键点容易踩坑环境变量管理开发环境和生产环境的API地址不一样需要在项目根目录下建.env.development和.env.production文件用import.meta.env.VITE_API_BASE_URL区分。千万别把接口地址硬编码在业务代码里。Axios的统一封装请求拦截器里自动附带token响应拦截器统一处理错误码比如401直接跳到登录页500弹错误提示。这个封装一定要做不然每个页面都写一遍try-catch会烦死。路由权限控制用Vue Router的meta字段标记当前路由需要哪些角色然后在beforeEach守卫里做校验。首次登录后从后端拿到用户的角色集合动态生成可访问的路由表。3.3 开发环境准备与版本匹配做Spring Boot Vue项目最让人抓狂的问题之一就是工具版本不兼容。我实测过一套相对稳定的版本组合分享给你们参考组件推荐版本说明JDK1.82.7.x/ 173.xSpring Boot版本决定JDK版本Maven3.8.x注意镜像源配置Node.js16.20 或 18.xVite 4需要14.18npm/yarn/pnpm推荐pnpm磁盘占用小安装快MySQL8.05.7也可以但8.0功能更全还有一个很容易被忽略的问题数据库连接依赖的驱动版本。MySQL 8.0的驱动类名是com.mysql.cj.jdbc.DriverURL需要加时区参数serverTimezoneAsia/Shanghai否则连接报错或者时间错乱。这些我都在后面的常见问题环节再展开。4. 数据库设计预约系统的地基4.1 核心表结构设计与关系数据库设计是整个项目里最需要花心思的部分比写代码重要十倍。预约类系统的核心表我拆成了八张用户表user、厨师表chef、厨师可用时段表chef_schedule、地址表address、预约单表reservation、订单表order_info、评价表review、钱包/结算表wallet。先说用户表和厨师表的关系。注意一个用户申请成为厨师不是往用户表里加一个身份字段那么简单。我采用的是用户与厨师一对一关联模型user表保留所有平台账号的基础信息chef表单独存厨师的资质信息、接单状态、平均评分等。这样用户表不用塞太多业务字段干净厨师的信息也有独立的管理入口。预约单表是整个系统的核心。它需要记录哪个用户、哪个厨师、服务日期、开始时间到结束时间、服务地址、服务类型上门/到店、费用明细食材费、服务费、平台佣金、状态。状态字段我会专门设计一个状态机后面详细说。创建预约单的DDL可以简化为这样CREATE TABLE reservation ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 下单用户ID, chef_id bigint NOT NULL COMMENT 厨师ID, serve_date date NOT NULL COMMENT 服务日期, start_time time NOT NULL COMMENT 开始时间, end_time time NOT NULL COMMENT 结束时间, address_id bigint NOT NULL COMMENT 服务地址ID, service_type tinyint NOT NULL COMMENT 1上门 2到店, total_amount decimal(10,2) NOT NULL COMMENT 总费用, deposit_amount decimal(10,2) NOT NULL COMMENT 定金金额, status tinyint NOT NULL COMMENT 状态0待付款 1待接单 2已接单 3服务中 4待评价 5已完成 6已取消 7退款中 8已退款, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_chef_date (chef_id, serve_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约单表;4.2 时间冲突与并发控制的库表设计预约系统最核心的难点是什么时间冲突。两个用户不能同时约同一个厨师的同一个时间段否则线下必炸锅。这个问题的解决不能只靠前端弹窗提示后端必须做校验。我的做法是在chef_schedule表中把厨师的可用时段预先拆分成固定的小段比如每2小时一段用户预约时直接锁定时段。具体表结构包含厨师ID、日期、开始时间、结束时间、是否可预约。用户提交预约后在事务中先查询该时段是否已被锁定再用UPDATE语句把状态改为已锁定依靠数据库的行级锁来防止并发问题。这里有个细节值得注意如果只用SELECT判断状态再执行INSERT两个并发请求可能同时查到可预约然后都下单成功。所以我建议用一个原子化的更新语句比如UPDATE chef_schedule SET status 1, version version 1 WHERE chef_id ? AND serve_date ? AND start_time ? AND status 0如果影响行数为0说明时段已经被占了直接返回该时段不可预约。这比分布式锁简单可靠得多性能也好。4.3 订单状态机的合理设计订单状态是整个项目中业务逻辑最复杂的环节必须一开始就用状态机来约束否则写到最后到处都是if-else改一个需求能引发一串bug。我设计的核心流转路径是这样待付款 - 待接单 - 已接单 - 服务中 - 待评价 - 已完成在这个主路径之外还有几条支线待付款超时30分钟未支付自动取消待接单状态下厨师可以拒单订单取消用户也可以主动取消已接单后如果距服务时间大于24小时用户或厨师可申请取消小于24小时则需客服介入毕设可以简化为不允许取消服务完成后用户超48小时未评价系统自动默认好评这个状态机最好用一张状态流转表固化下来写代码的时候就是查表填逻辑。别偷懒状态乱掉是预约类项目最大的风险。5. 后端核心实现与代码解析5.1 Spring Boot项目结构规划一个好的项目结构应该让人一眼就能看懂哪个类在哪个包里而不是所有Controller堆在一个包下面。我惯用的分包方式如下com.example.chefplatform ├── config // 配置类Security、Redis、MVC、Swagger ├── controller // 控制层按业务模块继续分包 ├── service // 接口层定义业务接口 │ └── impl // 实现类 ├── mapper // 数据访问层MyBatis-Plus的Mapper ├── entity // 实体类对应数据库表 ├── dto // 数据传输对象接收前端参数 ├── vo // 视图对象返回前端数据 ├── common // 通用类统一返回结果、异常处理、常量 └── utils // 工具类JWT、校验、日期处理等这个结构遵循了阿里Java开发手册的分层思想。它最大的好处是控制器非常薄只做参数接收和结果返回业务逻辑集中在service层方便做事务控制数据访问隔离在mapper层换数据库供应商不至于改动全局。5.2 统一响应封装与全局异常处理前后端联调最怕什么每个人返回的数据结构不一样。前端拿到的有时候是{code: 200}有时候是{status: true}这谁受得了。我从项目第一天就定了统一响应类Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }配合全局异常处理器把业务异常、参数校验异常、未知异常统一处理前端只要判断code是否为200即可。这一步看起来简单但能极大降低联调成本。5.3 预约下单接口的完整实现剖析预约下单是整个系统最核心的接口。它的逻辑链路是这样的先校验用户和厨师状态然后校验时段是否可选再锁定时段创建预约单最后发起支付流程。 整个流程必须放在一个数据库事务里其中锁定时段的操作必须用行锁避免并发问题。我给出一个简化的代码路径方便大家理解核心思路Transactional(rollbackFor Exception.class) public ReservationVO createReservation(CreateReservationRequest request) { // 1. 校验用户状态是否被拉黑、是否存在 User user userMapper.selectById(request.getUserId()); if (user null || user.getStatus() ! 1) { throw new BizException(用户不存在或已被禁用); } // 2. 查询厨师是否存在且处于可服务状态 Chef chef chefMapper.selectById(request.getChefId()); if (chef null || chef.getStatus() ! 1) { throw new BizException(厨师不存在或未通过审核); } // 3. 校验预约时间最早提前2小时最晚提前7天 validateServeTime(request.getServeDate(), request.getStartTime()); // 4. 锁定时段原子操作防止并发 int locked chefScheduleMapper.lockTimeSlot( request.getChefId(), request.getServeDate(), request.getStartTime() ); if (locked 0) { throw new BizException(该时段已被预约请选择其他时间); } // 5. 创建预约单 Reservation reservation buildReservation(request); reservation.setStatus(ReservationStatus.PENDING_PAYMENT.getCode()); reservationMapper.insert(reservation); // 6. 创建初始订单记录 createOrder(reservation); return buildReservationVO(reservation); }这个方法花了大量篇幅在处理校验和排他上而不是一头扎进去就创建订单。很多新手写这种接口最容易犯的毛病就是直接INSERT等数据出了问题才回头补校验那时候改起来非常麻烦。5.4 JWT认证与接口权限拦截JWT登录机制在前后端分离项目中几乎是标准做法。核心逻辑用户登录成功后后端用用户的ID和角色生成一个有效期为2小时的token返回给前端前端把token存到localStorage每次请求在Authorization头带上后端用拦截器解析token并校验身份。Spring Security配置的关键是放行白名单登录、注册、厨师列表查询、厨师详情其余接口一律需要认证。我的理解是JWT的设计初衷就是无状态所以服务端不要保存会话状态全靠签名验证这样服务端水平扩容的时候没有任何会话同步问题。但是为了提升安全性和用户体验我建议加一个Redis黑名单当用户登出或修改密码时把token加入黑名单这样即使token还没过期也无法再使用。这是一个很容易被忽略的安全细节。6. 前端核心页面与交互实现6.1 页面路由规划与权限守卫前端页面我拆成了三个端用户端、厨师端、管理后台。用户端包括首页、厨师列表、厨师详情、预约下单、订单列表、个人中心厨师端包括申请入驻、排期管理、接单列表、收益明细管理后台包括用户管理、厨师审核、订单管理、数据看板。三个端共用一个Vue项目用路由的meta字段区分角色权限{ path: /chef/schedule, name: ChefSchedule, component: () import(/views/chef/Schedule.vue), meta: { title: 排期管理, roles: [CHEF] // 仅厨师角色可访问 } }在全局守卫里校验用户角色和路由要求的角色是否匹配。这样做虽然三个端混在一个项目里稍显庞大但避免了维护三套代码的负担对于体量适中的项目很实用。如果项目规模再大可以考虑用Monorepo方式把三个端拆成三个独立包但这个项目没有必要。6.2 核心页面厨师列表与预约表单厨师列表页是这个项目的门面用户第一眼看到的就是它。我采用了条件搜索 卡片列表的方式左侧按菜系、服务类型、价格区间、评分做筛选右侧展示厨师卡片头像、名字、拿手菜标签、评分、接单量、起收费标准。列表数据通过Axios从后端分页拉取前端不做数据过滤这样压力全在后端SQL上前端只负责渲染。预约表单则是最强调交互细节的页面选择日期后前端先请求后端接口获取该厨师当天的可约时段再渲染成时间段列表。用户只能点选可用的时间段已经锁定的时段置灰。这里前端直接渲染不可选时段能从交互层面减少无效请求。预约表单提交还有一个细节防重复提交。用户在支付前可能会疯狂点击提交按钮导致后端同一时间创建多条预约单。我在前端做了按钮loading和禁用处理同时后端接口也做了幂等处理用前端生成的requestId作为唯一索引的一部分双管齐下才能保证安全。6.3 Vue 3组合式API的组织技巧项目中使用Composition API组织业务逻辑比Options API更灵活。每个页面的业务逻辑我习惯按功能点拆成独立函数再用生命周期钩子把它们串起来比如setup() { const chefList ref([]); const loading ref(false); const queryParams reactive({ page: 1, size: 10, cuisineType: , minScore: 0 }); // 拉取厨师列表的独立方法 async function fetchChefList() { loading.value true; try { const res await getChefList(queryParams); chefList.value res.data.records; } finally { loading.value false; } } // 搜索事件 function handleSearch() { queryParams.page 1; fetchChefList(); } onMounted(fetchChefList); return { chefList, loading, queryParams, handleSearch }; }这样的写法最大的优势是逻辑内聚。同一个功能点的数据、状态、方法都放在一起后期维护的时候改起来很清楚。我见过很多Vue 2项目的痛点就是data/methods/computed分离导致逻辑碎片化组合式API可以完美解决这个问题。7. 部署上线与运维要点7.1 前后端分离部署的整体方案开发完了要跑起来部署方案也得想清楚。一个标准的Spring Boot Vue部署架构是前端构建成静态文件部署到Nginx后端打成Jar包用systemd或Docker跑在服务器上数据库用MySQL缓存用Redis。前端通过Nginx反向代理/api路径到后端服务。我先说一个常见的误区开发环境的前端通过Vite的proxy代理解决跨域但生产环境其实不需要前端代理因为Nginx本来就会把来自同一个域名的请求转发到后端所以从前端视角看所有请求都是同源的。Nginx的关键配置如下server { listen 80; server_name your-domain.com; # 前端静态资源 root /var/www/dist; index index.html; # 解决前端路由刷新404的问题 location / { try_files $uri $uri/ /index.html; } # 反向代理后端接口 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里最值得注意的就是try_files那行。Vue是单页应用路由切换靠的是前端history API如果用户直接访问/chef/schedule这个路径Nginx找不到对应文件会返回404加了try_files正则命中后就会回退到index.html由前端路由接管。7.2 服务器环境准备服务器配置我建议最低2核4G内存起步。这个项目规模不算重2核4G足够跑Redis、MySQL和Spring Boot了。环境准备大致分为三步第一步安装JDK和Maven配置好环境变量第二步安装MySQL和Redis设置好密码和防火墙第三步把前端构建产物上传到/var/www/dist把后端Jar包放到/opt/app目录下用Systemd管理进程。Spring Boot的启动脚本我习惯用systemd来管理好处是开机自启、崩溃自动重启、日志统一由journald管理。关键配置如下[Unit] DescriptionChef Platform Backend Afternetwork.target mysqld.service redis.service [Service] Userapp WorkingDirectory/opt/app ExecStart/usr/bin/java -jar /opt/app/chef-platform.jar --spring.profiles.activeprod Restartalways RestartSec10 [Install] WantedBymulti-user.target7.3 上线前的安全检查清单每次上线前我都会过一遍这份检查清单建议你们也收藏数据库账号是否用了最小权限千万别用root账号连接业务库。Redis是否设置了密码没密码的Redis在公网上会被秒级入侵。Spring Boot的Actuator端点是否对外暴露生产环境需要关闭或加权限控制。前端打包产物里是否残留了console.log和调试信息后端日志是否分级错误日志要独立文件方便排查。HTTPS证书是否配置现在浏览器对HTTP的警告越来越多建议直接用HTTPS。8. 常见问题与排查技巧实录8.1 跨域问题的完整排查思路前后端分离项目里跨域问题高居榜首。虽然前面提到Nginx反代能在生产环境解决但开发环境下前端跑在5173端口后端跑在8080端口跨域是必然的。最让我记忆深刻的经验是开发环境的跨域不能在Spring Boot里用CrossOrigin草草解决。那样做虽然能跑通但生产环境如果也放开所有跨域等于把接口暴露给任何来源的请求非常危险。我建议开发环境用前端代理生产环境用Nginx同源解决两边都不要在后端放开跨域。具体做法是在Vite的配置文件里加export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });8.2 预约超时未支付的自动取消预约单创建后用户迟迟不付款怎么办不能一直占着厨师的时段否则对厨师和其他用户都不公平。我的方案是创建预约单时同时往Redis写入一条带过期时间的key比如reservation_ {id}过期时间为30分钟另外用xxl-job或者Spring自带的Scheduled定时任务每分钟扫描一次超过30分钟未支付的订单将状态改为已取消并释放厨师的时段。这两种方案各有优缺点Redis过期通知的方式实时性好但依赖Redis的key过期事件存在消费延迟定时任务简单可靠轮询扫描即可。我最终采用的是定时任务为主、Redis缓存做实时提示的折中方案。8.3 数据库连接时区与中文乱码这个坑几乎是连踩必中。MySQL 8.0的默认时区和Spring Boot默认时区不一致会导致数据库存的时间比实际时间早8小时。我建议在JDBC连接串上显式指定jdbc:mysql://localhost:3306/chef_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse中文乱码则和表结构的字符集有关建表时务必统一使用utf8mb4。项目早期的历史表如果是utf8插入emoji表情会直接报错因为utf8字符集不支持4字节的emoji。所以从一开始就用utf8mb4最省心。8.4 事务失效的典型陷阱事务这事儿看起来简单实际上坑多。我遇到过一个最典型的问题在同一个类内部一个方法调用另一个带Transactional注解的方法事务失效了。原因是Spring的事务是基于AOP代理实现的同类内部调用不会走代理所以注解不生效。解决方案很简单把需要事务的方法拆分到不同的Service类中通过注入的方式调用。另外一个容易被忽视的问题是事务方法内部不能用try-catch把异常吞掉。一旦异常被捕获事务拦截器就感知不到异常了自然不会回滚。正确做法是捕获异常后抛出运行时异常触发回滚。8.5 前端打包后路由404与资源路径出错前端代码在本地跑得飞起一打包部署到Nginx上两个问题立刻冒出来刷新页面404静态资源加载失败。404的问题上面Nginx配置已经解决了。静态资源加载失败原因通常是构建时base路径设置不对。Vite默认的base是/如果你的站点是部署在域名根路径下没问题如果你部署在子路径比如http://domain.com/chef/必须把base改成/chef/。另外Vue Router要设置成createWebHistory(import.meta.env.BASE_URL)确保路由根路径和部署路径一致。还有一个常见的细节部署后CSS中引用的图片路径失效通常也是在写CSS时用了绝对路径应该改成相对路径或者统一走静态资源导入。9. 实操总结与经验延伸做完整套系统的开发我自己最大的体会是这个项目表面上是一个预约平台实际上练的是完整的Web全链路能力。从需求分析、数据库设计、后端API、前端交互、部署上线、问题排查每个环节都会暴露问题每个问题都能反推回前面的设计决策。这也正是Spring Boot Vue组合适合学习和实践的根本原因——它是一个真正完整的闭环。最后再分享一个对未来扩展有用的建议不要被当前需求限制死。做数据库设计时给用户表预留扩展字段给订单表预留外部单号字段给厨师表预留结算信息表。等到真的需要接入在线支付、优惠券、会员体系、骑手调度等功能时你不会因为表结构缺字段而推翻重来。我就是当初没留够扩展空间后期加功能时做了好几次大迁移真金白银换来的教训。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门