温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片!
一、 项目背景与意义
随着生活节奏加快和“懒人经济”的兴起,上门代厨服务作为一种新兴的O2O(线上到线下)模式,正逐渐受到都市人群的青睐。用户通过线上平台预约厨师上门烹饪,享受个性化、高品质的家宴服务。然而,该行业在快速发展的同时,也面临着服务流程不规范、信息不对称、管理效率低下等痛点。
因此,开发一套集用户端、厨师端、管理端于一体的上门代厨管理系统显得尤为必要。该系统旨在:
- 标准化服务流程:将预约、接单、备菜、烹饪、支付、评价等环节线上化、规范化。
- 提升用户体验:为用户提供便捷的预约、支付、沟通和售后渠道。
- 赋能厨师与平台:帮助厨师高效管理订单与时间,辅助平台进行人员调度、财务核算与数据分析。
- 探索商业模式:为平台运营者提供数据支撑,助力业务拓展与决策。
本项目的实现,不仅是对SpringBoot全栈技术的一次综合实践,也为解决实际生活服务领域的数字化问题提供了可行方案。
二、 技术栈选型与架构
本项目采用前后端分离的架构模式,确保系统的高内聚、低耦合与可扩展性。
2.1 后端技术栈 (SpringBoot)
- 核心框架:Spring Boot 2.7+, 提供快速启动和自动配置。
- Web框架:Spring MVC, 处理HTTP请求与响应。
- 数据持久层:
- ORM框架:MyBatis-Plus, 简化CRUD操作。
- 数据库:MySQL 8.0, 存储核心业务数据。
- 连接池:HikariCP, 高性能数据库连接池。
- 安全与认证:
- Spring Security + JWT (JSON Web Token), 实现无状态登录认证与权限控制。
- 其他组件:
- Redis: 用作缓存(菜品信息、用户会话)和分布式锁(防止订单超卖)。
- RabbitMQ / RocketMQ: 异步处理订单消息、发送通知(短信、微信模板消息)。
- OSS对象存储(如阿里云OSS): 存储用户头像、厨师资质、菜品图片。
- Swagger / Knife4j: 自动生成并可视化API文档。
2.2 前端技术栈 (Vue.js)
- 核心框架:Vue 3 (Composition API) 或 Vue 2, 构建用户交互界面。
- 构建工具:Vite 或 Webpack, 项目打包与热更新。
- UI组件库:Element Plus (Vue 3) 或 Element UI (Vue 2), 快速搭建管理后台。
- 状态管理:Pinia (Vue 3) 或 Vuex (Vue 2), 管理跨组件共享状态。
- 路由:Vue Router, 实现单页面应用(SPA)路由跳转。
- HTTP客户端:Axios, 封装与后端的API通信,统一处理请求拦截、响应拦截和错误。
2.3 移动端技术栈 (微信小程序)
- 开发框架:原生小程序开发 或 Uni-app / Taro等多端框架。
- UI组件:Vant Weapp 或官方组件, 提供丰富的移动端UI。
- 地图与定位:微信小程序地图API, 用于厨师上门导航、服务范围展示。
- 支付:微信支付API, 实现小程序内安全支付。
- 云开发(可选):利用微信云开发快速实现部分后端能力,如文件存储、云函数。
2.4 系统架构图(Mermaid)
flowchart TD subgraph Client [客户端] direction LR C1[微信小程序 用户/厨师端] C2[Web管理后台 Vue.js] end subgraph Gateway [网关层] G[Nginx<br/>反向代理 & 负载均衡] end subgraph Backend [后端服务] B[SpringBoot应用集群] B --> DB[(MySQL)] B --> Cache[(Redis)] B --> MQ[RabbitMQ] B --> OSS[对象存储 OSS] end Client --> G G --> B style Client fill:#e1f5fe style Gateway fill:#f3e5f5 style Backend fill:#e8f5e8三、 核心功能模块与数据库设计
3.1 核心功能模块
- 用户端(小程序):注册登录、菜品浏览与搜索、厨师筛选与查看、在线预约与支付、订单状态跟踪、在线沟通、服务评价。
- 厨师端(小程序):资质认证、服务时间管理、订单接单/拒单、上门导航、完成服务确认、收入查看。
- 管理后台(Web):用户/厨师信息审核与管理、菜品/服务类别管理、订单全流程监控、财务对账、数据统计报表、系统配置。
3.2 核心表结构示例(MySQL)
-- 用户表 CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `phone` varchar(11) NOT NULL COMMENT '手机号', `avatar` varchar(500) DEFAULT NULL COMMENT '头像', `user_type` tinyint NOT NULL COMMENT '用户类型 0-普通用户 1-厨师 2-管理员', `status` tinyint DEFAULT '1' COMMENT '状态 0-禁用 1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`) ) ENGINE=InnoDB COMMENT='用户表'; -- 厨师信息扩展表 CREATE TABLE chef_info ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT '关联用户ID', real_name varchar(20) NOT NULL COMMENT '真实姓名', id_card varchar(18) COMMENT '身份证号', certificate_imgs text COMMENT '资质证书图片(JSON数组)', service_city varchar(100) COMMENT '服务城市', introduction text COMMENT '个人介绍', avg_score decimal(3,2) DEFAULT '5.00' COMMENT '平均评分', order_count int DEFAULT 0 COMMENT '接单数', PRIMARY KEY (id), UNIQUE KEY uk_user_id (user_id) ) ENGINE=InnoDB COMMENT='厨师信息表'; -- 菜品/服务表 CREATE TABLE dish_service ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT '菜品/服务名称', category_id bigint COMMENT '分类ID', price decimal(10,2) NOT NULL COMMENT '价格', main_image varchar(500) COMMENT '主图', description text COMMENT '描述', status tinyint DEFAULT '1' COMMENT '状态 0-下架 1-上架', PRIMARY KEY (id) ) ENGINE=InnoDB COMMENT='菜品服务表'; -- 订单表(核心) CREATE TABLE order_info ( id varchar(32) NOT NULL COMMENT '订单号', user_id bigint NOT NULL COMMENT '用户ID', chef_id bigint COMMENT '接单厨师ID', total_amount decimal(10,2) NOT NULL COMMENT '订单总金额', order_status tinyint NOT NULL COMMENT '状态 0-待支付 1-待接单 2-已接单 3-服务中 4-已完成 5-已取消 6-退款中', appointment_time datetime NOT NULL COMMENT '预约上门时间', address varchar(500) NOT NULL COMMENT '服务地址', remark varchar(500) COMMENT '用户备注', create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime COMMENT '支付时间', finish_time datetime COMMENT '完成时间', PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_chef_id (chef_id), KEY idx_status (order_status) ) ENGINE=InnoDB COMMENT='订单主表';四、 核心代码示例
4.1 后端核心代码 (SpringBoot)
1. 统一响应封装 (Result.java)
import lombok.Data; import java.io.Serializable; @Data public class Result<T> implements Serializable { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("success"); result.setData(data); return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.setCode(500); result.setMsg(msg); return result; } // 其他静态方法... }2. 全局异常处理 (GlobalExceptionHandler.java)
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result<Object> handleException(Exception e, HttpServletRequest request) { // 记录日志 log.error("请求URI: {}, 异常信息: ", request.getRequestURI(), e); // 返回友好错误信息 return Result.error("系统繁忙,请稍后再试"); } @ExceptionHandler(BusinessException.class) public Result<Object> handleBusinessException(BusinessException e) { return Result.error(e.getMessage()); } }3. 订单状态机服务 (OrderStateMachineService.java) - 关键业务逻辑
import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Slf4j public class OrderStateMachineService { @Autowired private OrderInfoMapper orderInfoMapper; @Autowired private RedisTemplate<String, String> redisTemplate; /** 厨师接单 */ @Transactional(rollbackFor = Exception.class) public Result<String> acceptOrder(String orderId, Long chefId) { // 1. 查询订单 OrderInfo order = orderInfoMapper.selectById(orderId); if (order == null) { return Result.error("订单不存在"); } // 2. 状态校验:必须是“待接单”状态 if (order.getOrderStatus() != OrderStatusEnum.WAITING_FOR_ACCEPT.getCode()) { return Result.error("订单当前状态不可接单"); } // 3. 使用Redis分布式锁,防止并发接单 String lockKey = "order:accept:" + orderId; Boolean lockAcquired = redisTemplate.opsForValue() .setIfAbsent(lockKey, chefId.toString(), 10, TimeUnit.SECONDS); if (Boolean.FALSE.equals(lockAcquired)) { return Result.error("订单正在被处理,请稍后"); } try { // 4. 更新订单状态和厨师ID order.setChefId(chefId); order.setOrderStatus(OrderStatusEnum.ACCEPTED.getCode()); orderInfoMapper.updateById(order); // 5. 发送MQ消息,通知用户接单成功 rabbitTemplate.convertAndSend("order.exchange", "order.accepted", orderId); log.info("厨师{}接单成功,订单号: {}", chefId, orderId); return Result.success("接单成功"); } finally { // 6. 释放锁 redisTemplate.delete(lockKey); } } }4.2 前端核心代码 (Vue3 + Element Plus)
1. 订单列表页面组件 (OrderList.vue) - 组合式API
<template> <div class="order-list"> <el-table :data="tableData" style="width: 100%"> <el-table-column prop="id" label="订单号" width="180" /> <el-table-column prop="totalAmount" label="金额"> <template #default="scope"> ¥{{ scope.row.totalAmount }} </template> </el-table-column> <el-table-column prop="orderStatus" label="状态"> <template #default="scope"> <el-tag :type="getStatusType(scope.row.orderStatus)"> {{ getStatusText(scope.row.orderStatus) }} </el-tag> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" /> <el-table-column label="操作" width="150"> <template #default="scope"> <el-button size="small" @click="handleView(scope.row)">查看</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="total" @current-change="handlePageChange" /> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { getOrderList } from '@/api/order' import { ElMessage } from 'element-plus' const tableData = ref([]) const currentPage = ref(1) const pageSize = ref(10) const total = ref(0) const fetchData = async () => { try { const params = { page: currentPage.value, size: pageSize.value } const res = await getOrderList(params) tableData.value = res.data.records total.value = res.data.total } catch (error) { ElMessage.error('获取订单列表失败') } } const getStatusType = (status) => { const map = { 0: 'info', 1: 'warning', 2: '', 3: 'primary', 4: 'success', 5: 'danger' } return map[status] || '' } const getStatusText = (status) => { const map = { 0: '待支付', 1: '待接单', 2: '已接单', 3: '服务中', 4: '已完成', 5: '已取消' } return map[status] || '未知' } const handleView = (row) => { // 跳转到订单详情页 } onMounted(() => { fetchData() }) </script>4.3 小程序端核心代码 (微信小程序 - WXML + JS)
1. 首页菜品列表渲染 (index.wxml)
<!-- index.wxml --> <view class="page"> <!-- 搜索框 --> <view class="search-bar"> <input placeholder="搜索菜品或厨师" bindinput="onSearchInput" /> </view> <!-- 轮播图 --> <swiper indicator-dots autoplay interval="3000"> <swiper-item wx:for="{{bannerList}}" wx:key="id"> <image src="{{item.imageUrl}}" mode="aspectFill"></image> </swiper-item> </swiper> <!-- 菜品分类 --> <scroll-view scrol