毕业设计实战:基于SpringBoot+Vue的餐饮管理系统,从菜品管理到订单处理全流程拆解!
当初做餐饮管理系统毕设时,最头疼的就是库存管理——用户下单后库存没实时更新,结果同一道菜超卖了好几次!还有订单状态流转、会员积分计算这些坑,调试到凌晨三点才搞定。今天就把餐饮管理系统从需求分析到测试上线的完整流程拆解清楚,跟着做就能少走弯路!
一、先搞懂“餐饮管理系统到底管什么”!
刚开始我以为就是简单的点餐系统,结果导师说要包含“供应商管理、库存管理、会员体系、菜品评价”完整生态链。后来才明白,餐饮系统最重要的是“数据流”和“状态流转”——从供应商进货→入库→上架→用户下单→库存扣减→积分计算,每个环节都不能掉链子。
1. 核心角色&功能拆解(真实场景版)
系统有三类核心用户:管理员、员工、普通用户(顾客):
- 管理员端(后台管理核心):
- 菜品管理:维护菜品信息(名称/价格/图片/库存)、设置上架状态、管理菜品分类(热菜/凉菜/主食等)
- 订单管理:查看所有订单、处理退款申请、统计销售数据(日/月/年报表)
- 会员管理:管理用户信息、设置会员等级(普通/黄金/铂金)、调整积分规则
- 供应商管理:维护供应商信息(名称/联系方式/供应物品)、管理采购记录
- 内容管理:发布公告、管理论坛帖子、维护轮播图
- 员工端(后厨/前台操作):
- 订单处理:查看待处理订单、标记制作进度(待制作/制作中/已完成)
- 库存管理:查看库存预警(库存不足的菜品)、提交采购申请
- 菜品管理:协助更新菜品状态(今日特价/售罄标记)
- 用户端(顾客体验关键):
- 菜品浏览:按分类筛选、查看菜品详情(图片/价格/评价)、收藏喜欢的菜品
- 在线下单:加入购物车、选择配送方式(堂食/外卖)、在线支付(模拟支付)
- 个人中心:查看订单历史、管理收货地址、查看积分余额、参与菜品评价
2. 需求分析避坑指南(血泪经验!)
- 库存同步是重中之重:用户下单必须实时扣库存,取消订单要返还库存。我当初没做“库存预扣”机制,结果高并发下超卖了!
- 订单状态流转要清晰:设计状态机:待支付→已支付(待制作)→制作中→已完成→已评价。每个状态变更都要记录操作人和时间。
- 积分计算规则要明确:比如“实付金额×10% = 获得积分”“不同会员等级积分倍数不同”。提前写清楚规则,编码时不会乱。
3. 可行性分析(导师最爱问的)
- 技术可行性:SpringBoot + Vue + MySQL是经典组合,资料多、社区活跃。别用太新的技术栈,比如SpringBoot 3.x可能有些依赖还不稳定。
- 经济可行性:开发工具全免费(IDEA社区版、VSCode、MySQL),部署可以用学生云服务器(腾讯云/阿里云学生机一年才100多)。
- 操作可行性:界面参考美团/饿了么,用户学习成本低。我找室友测试,下单流程1分钟就学会了。
二、技术选型:前后端分离是趋势
传统的JSP项目已经过时了,现在都是前后端分离。我推荐 SpringBoot 2.7 + Vue 3 + Element Plus + MySQL 8.0 这套组合,既现代又稳定。
1. 技术栈详解(为什么选它们)
| 技术 | 作用 | 避坑提醒 |
|---|---|---|
| SpringBoot 2.7 | 后端框架,快速搭建REST API | 别用3.0+,部分组件兼容性还不够好 |
| Vue 3 + TypeScript | 前端框架,组件化开发 | 用Composition API比Options API更灵活 |
| Element Plus | UI组件库,快速搭建美观界面 | 注意按需引入,不然打包体积太大 |
| MySQL 8.0 | 数据存储,支持JSON类型 | 一定设utf8mb4编码,支持emoji表情 |
| MyBatis-Plus | 简化数据库操作 | 自动生成代码好用,但复杂查询建议手写SQL |
| Redis (可选) | 缓存菜品数据、购物车 | 如果数据量不大,MySQL也能撑住 |
2. 环境搭建(一步一步来)
后端环境:
- 用IDEA新建SpringBoot项目,选2.7.x版本
- 勾选Web、MySQL、MyBatis依赖
- 配置
application.yml:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/restaurant_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发时看SQL日志
前端环境:
- 安装Node.js(16.x以上)
- 用Vite创建Vue项目:
npm create vue@latest - 安装Element Plus:
npm install element-plus - 安装axios:
npm install axios
3. 项目结构规划
backend/ # SpringBoot后端
├── src/main/java/com/restaurant/
│ ├── controller/ # 控制器
│ ├── entity/ # 实体类
│ ├── mapper/ # 数据访问层
│ ├── service/ # 业务逻辑
│ └── config/ # 配置类
frontend/ # Vue前端
├── src/
│ ├── views/ # 页面组件
│ ├── components/ # 公共组件
│ ├── api/ # 接口定义
│ └── router/ # 路由配置
三、数据库设计:餐饮系统的核心
餐饮系统的数据库设计要特别注意“库存一致性”和“订单完整性”。我设计了12张核心表,下面是关键表结构:
1. 核心表结构(SQL示例)
菜品表(核心中的核心):
CREATE TABLE `dish` (
`id` int NOT NULL AUTO_INCREMENT,
`dish_number` varchar(50) NOT NULL COMMENT '菜品编号(唯一)',
`dish_name` varchar(100) NOT NULL COMMENT '菜品名称',
`dish_image` varchar(255) DEFAULT NULL COMMENT '菜品图片路径',
`category_id` int NOT NULL COMMENT '分类ID(关联分类表)',
`original_price` decimal(10,2) NOT NULL COMMENT '原价',
`current_price` decimal(10,2) NOT NULL COMMENT '现价',
`stock` int NOT NULL DEFAULT 0 COMMENT '库存',
`sales_count` int DEFAULT 0 COMMENT '销量',
`click_count` int DEFAULT 0 COMMENT '点击量',
`description` text COMMENT '菜品描述',
`status` tinyint DEFAULT 1 COMMENT '状态(1-上架 0-下架)',
`is_hot` tinyint DEFAULT 0 COMMENT '是否推荐(1-是 0-否)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_dish_number` (`dish_number`),
KEY `idx_category` (`category_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表';
订单表(状态流转关键):
CREATE TABLE `order` (
`id` int NOT NULL AUTO_INCREMENT,
`order_number` varchar(50) NOT NULL COMMENT '订单号(唯一)',
`user_id` int NOT NULL COMMENT '用户ID',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`discount_amount` decimal(10,2) DEFAULT 0 COMMENT '优惠金额',
`actual_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
`order_status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(1-待支付 2-已支付 3-制作中 4-已完成 5-已取消 6-退款中)',
`payment_method` tinyint DEFAULT NULL COMMENT '支付方式(1-微信 2-支付宝 3-余额)',
`delivery_method` tinyint DEFAULT 1 COMMENT '配送方式(1-堂食 2-外卖)',
`delivery_address` varchar(500) DEFAULT NULL COMMENT '配送地址',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`remark` varchar(500) DEFAULT NULL COMMENT '订单备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`complete_time` datetime DEFAULT NULL COMMENT '完成时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_number` (`order_number`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`order_status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
订单明细表(记录每个菜品的购买情况):
CREATE TABLE `order_item` (
`id` int NOT NULL AUTO_INCREMENT,
`order_id` int NOT NULL COMMENT '订单ID',
`dish_id` int NOT NULL COMMENT '菜品ID',
`dish_name` varchar(100) NOT NULL COMMENT '菜品名称(下单时的快照)',
`dish_image` varchar(255) DEFAULT NULL COMMENT '菜品图片',
`price` decimal(10,2) NOT NULL COMMENT '单价',
`quantity` int NOT NULL COMMENT '数量',
`subtotal` decimal(10,2) NOT NULL COMMENT '小计',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_dish_id` (`dish_id`),
CONSTRAINT `fk_order_item_order` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`),
CONSTRAINT `fk_order_item_dish` FOREIGN KEY (`dish_id`) REFERENCES `dish` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
2. 表关联查询示例
-- 查询用户订单详情(包含菜品信息)
SELECT
o.order_number,
o.create_time,
o.order_status,
o.total_amount,
o.actual_amount,
oi.dish_name,
oi.quantity,
oi.price,
oi.subtotal
FROM `order` o
LEFT JOIN order_item oi ON o.id = oi.order_id
WHERE o.user_id = 1
ORDER BY o.create_time DESC;
-- 查询热销菜品TOP10
SELECT
d.dish_name,
d.current_price,
d.dish_image,
COUNT(oi.id) as sales_count,
SUM(oi.quantity) as total_quantity
FROM dish d
LEFT JOIN order_item oi ON d.id = oi.dish_id
LEFT JOIN `order` o ON oi.order_id = o.id AND o.order_status IN (2,3,4) -- 已支付、制作中、已完成
WHERE d.status = 1 -- 上架状态
GROUP BY d.id
ORDER BY sales_count DESC
LIMIT 10;
3. 库存更新逻辑(关键!)
-- 下单时扣减库存(使用乐观锁防止超卖)
UPDATE dish
SET stock = stock - #{quantity},
version = version + 1,
sales_count = sales_count + #{quantity}
WHERE id = #{dishId}
AND stock >= #{quantity}
AND version = #{currentVersion};
-- 取消订单时恢复库存
UPDATE dish
SET stock = stock + #{quantity},
version = version + 1
WHERE id = #{dishId};
四、核心功能实现:三个重点模块
1. 菜品管理模块(管理员端)
页面设计要点:
- 菜品列表:支持按分类筛选、按状态筛选(上架/下架)、按是否推荐筛选
- 添加菜品:表单包含菜品名称、分类、价格、库存、图片上传、详细描述
- 图片上传:限制2MB以内,支持jpg/png,上传后生成缩略图
关键代码(图片上传):
@RestController
@RequestMapping("/api/dish")
public class DishController {
@PostMapping("/upload")
public Result uploadImage(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.error("请选择图片");
}
// 校验文件类型
String contentType = file.getContentType();
if (!contentType.startsWith("image/")) {
return Result.error("只支持图片格式");
}
// 校验文件大小(2MB)
if (file.getSize() > 2 * 1024 * 1024) {
return Result.error("图片大小不能超过2MB");
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString() + fileExtension;
// 按日期分目录存储
String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date());
String uploadDir = "/static/upload/dish/" + dateDir + "/";
String filePath = uploadDir + fileName;
try {
// 创建目录
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 保存文件
file.transferTo(new File(dir, fileName));
// 返回访问路径
String accessPath = "/upload/dish/" + dateDir + "/" + fileName;
return Result.success("上传成功", accessPath);
} catch (IOException e) {
e.printStackTrace();
return Result.error("上传失败");
}
}
}
2. 在线下单模块(用户端)
购物车设计:
<template>
<div class="cart-container">
<!-- 购物车列表 -->
<div v-for="item in cartItems" :key="item.dishId" class="cart-item">
<img :src="item.dishImage" :alt="item.dishName" />
<div class="item-info">
<h4>{{ item.dishName }}</h4>
<p>¥{{ item.price }}</p>
</div>
<div class="quantity-control">
<button @click="decreaseQuantity(item)">-</button>
<span>{{ item.quantity }}</span>
<button @click="increaseQuantity(item)">+</button>
</div>
<div class="subtotal">¥{{ (item.price * item.quantity).toFixed(2) }}</div>
<button @click="removeItem(item)" class="remove-btn">×</button>
</div>
<!-- 结算栏 -->
<div class="cart-footer">
<div class="total-amount">
总计:<span>¥{{ totalAmount.toFixed(2) }}</span>
</div>
<button
:disabled="cartItems.length === 0"
@click="checkout"
class="checkout-btn"
>
去结算 ({{ totalQuantity }}件)
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useCartStore } from '@/stores/cart'
const cartStore = useCartStore()
// 计算属性
const cartItems = computed(() => cartStore.items)
const totalAmount = computed(() => cartStore.totalAmount)
const totalQuantity = computed(() => cartStore.totalQuantity)
// 修改数量
const increaseQuantity = (item: CartItem) => {
cartStore.updateQuantity(item.dishId, item.quantity + 1)
}
const decreaseQuantity = (item: CartItem) => {
if (item.quantity > 1) {
cartStore.updateQuantity(item.dishId, item.quantity - 1)
}
}
// 移除商品
const removeItem = (item: CartItem) => {
cartStore.removeItem(item.dishId)
}
// 结算
const checkout = () => {
// 跳转到订单确认页面
router.push('/checkout')
}
</script>
下单接口(库存扣减+订单生成):
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
@Autowired
private DishMapper dishMapper;
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Override
public Result createOrder(OrderCreateDTO dto, Integer userId) {
// 1. 校验库存(先查后锁)
List<OrderItemDTO> items = dto.getItems();
Map<Integer, Integer> stockMap = new HashMap<>();
for (OrderItemDTO item : items) {
Dish dish = dishMapper.selectById(item.getDishId());
if (dish == null) {
return Result.error("菜品不存在: " + item.getDishName());
}
if (dish.getStock() < item.getQuantity()) {
return Result.error("库存不足: " + dish.getDishName());
}
stockMap.put(dish.getId(), dish.getVersion()); // 记录版本号
}
// 2. 扣减库存(使用乐观锁)
for (OrderItemDTO item : items) {
Integer version = stockMap.get(item.getDishId());
int updateCount = dishMapper.decreaseStock(
item.getDishId(),
item.getQuantity(),
version
);
if (updateCount == 0) {
throw new RuntimeException("库存扣减失败,请重试");
}
}
// 3. 生成订单
String orderNumber = "ORD" + System.currentTimeMillis() + RandomUtil.randomNumbers(4);
Order order = new Order();
order.setOrderNumber(orderNumber);
order.setUserId(userId);
order.setTotalAmount(dto.getTotalAmount());
order.setActualAmount(dto.getActualAmount());
order.setDiscountAmount(dto.getDiscountAmount());
order.setDeliveryMethod(dto.getDeliveryMethod());
order.setDeliveryAddress(dto.getDeliveryAddress());
order.setContactPhone(dto.getContactPhone());
order.setRemark(dto.getRemark());
order.setOrderStatus(1); // 待支付
orderMapper.insert(order);
// 4. 保存订单明细
for (OrderItemDTO item : items) {
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
orderItem.setDishId(item.getDishId());
orderItem.setDishName(item.getDishName());
orderItem.setDishImage(item.getDishImage());
orderItem.setPrice(item.getPrice());
orderItem.setQuantity(item.getQuantity());
orderItem.setSubtotal(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
orderItemMapper.insert(orderItem);
}
return Result.success("订单创建成功", orderNumber);
}
}
3. 会员积分模块(营销亮点)
积分规则配置:
@Component
public class PointsConfig {
// 积分获取规则:实付金额 × 积分率
@Value("${points.rate:0.1}")
private BigDecimal pointsRate;
// 不同会员等级积分倍数
private Map<Integer, BigDecimal> levelMultiplier = new HashMap<>();
public PointsConfig() {
levelMultiplier.put(1, BigDecimal.ONE); // 普通会员 ×1
levelMultiplier.put(2, new BigDecimal("1.2")); // 黄金会员 ×1.2
levelMultiplier.put(3, new BigDecimal("1.5")); // 铂金会员 ×1.5
}
// 计算应得积分
public BigDecimal calculatePoints(BigDecimal actualAmount, Integer memberLevel) {
BigDecimal basePoints = actualAmount.multiply(pointsRate);
BigDecimal multiplier = levelMultiplier.getOrDefault(memberLevel, BigDecimal.ONE);
return basePoints.multiply(multiplier).setScale(0, RoundingMode.DOWN);
}
}
积分发放(订单完成后):
@Async // 异步处理,不影响主流程
@Transactional
public void grantPointsAfterOrder(Integer orderId) {
Order order = orderMapper.selectById(orderId);
if (order == null || order.getOrderStatus() != 4) { // 不是已完成状态
return;
}
User user = userMapper.selectById(order.getUserId());
PointsConfig pointsConfig = new PointsConfig();
// 计算本次获得积分
BigDecimal pointsEarned = pointsConfig.calculatePoints(
order.getActualAmount(),
user.getMemberLevel()
);
// 更新用户积分
user.setPoints(user.getPoints().add(pointsEarned));
user.setTotalPoints(user.getTotalPoints().add(pointsEarned));
userMapper.updateById(user);
// 记录积分明细
PointsRecord record = new PointsRecord();
record.setUserId(user.getId());
record.setOrderId(orderId);
record.setOrderNumber(order.getOrderNumber());
record.setPoints(pointsEarned);
record.setChangeType(1); // 1-增加
record.setDescription("订单消费获得积分");
record.setBalanceAfter(user.getPoints());
pointsRecordMapper.insert(record);
}
五、系统测试:这些坑一定要避开
1. 功能测试用例(示例)
下单流程测试:
| 测试场景 | 操作步骤 | 预期结果 | 实际结果 |
|---|---|---|---|
| 正常下单 | 选择菜品A(库存10)→下单数量3→提交 | 订单创建成功,菜品A库存变为7 | |
| 超库存下单 | 选择菜品A(库存10)→下单数量15→提交 | 提示“库存不足” | |
| 并发下单 | 两个用户同时下单菜品A(库存10),各买6个 | 只有一个订单成功,另一个提示库存不足 |
库存一致性测试:
-- 验证库存数据是否正确
SELECT
d.dish_name,
d.stock as 系统库存,
d.total_stock as 总进货量,
(SELECT SUM(quantity) FROM order_item oi
JOIN `order` o ON oi.order_id = o.id
WHERE oi.dish_id = d.id AND o.order_status IN (2,3,4)) as 已售出数量,
(d.total_stock - d.stock - IFNULL(已售出数量, 0)) as 库存差异
FROM dish d
HAVING 库存差异 != 0;
2. 并发测试(重点!)
使用JMeter模拟高并发下单:
// 使用分布式锁或数据库乐观锁解决超卖
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
@Transactional
public Result createOrderWithLock(OrderCreateDTO dto, Integer userId) {
String lockKey = "order:lock:dish:" + dto.getItems().hashCode();
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待3秒,锁有效期10秒
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!locked) {
return Result.error("系统繁忙,请稍后重试");
}
// 执行业务逻辑
return createOrder(dto, userId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.error("系统异常");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
3. 边界测试
- 菜品价格为0或负数
- 订单金额超过数据库字段范围(DECIMAL(10,2)最大99999999.99)
- 用户输入超长字符串(地址超过500字符)
- 上传非图片文件
六、部署上线(让导师看到你的专业性)
1. 前端部署(Vue项目)
# 1. 打包项目
npm run build
# 2. 生成的dist目录放到Nginx服务器
# Nginx配置示例:
server {
listen 80;
server_name your-domain.com;
location / {
root /var/www/restaurant-frontend/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# 代理API请求到后端
location /api/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
2. 后端部署(SpringBoot)
# 1. 打包为Jar
mvn clean package -DskipTests
# 2. 上传到服务器
scp target/restaurant-backend.jar user@server:/app/
# 3. 编写启动脚本
#!/bin/bash
java -jar -Dspring.profiles.active=prod restaurant-backend.jar
# 4. 使用systemd管理服务
# /etc/systemd/system/restaurant.service
[Unit]
Description=Restaurant Management System
After=network.target
[Service]
User=appuser
WorkingDirectory=/app
ExecStart=/usr/bin/java -jar restaurant-backend.jar
SuccessExitStatus=143
Restart=always
[Install]
WantedBy=multi-user.target
七、答辩准备:三个必杀技
-
演示流程要流畅:
- 用户注册登录→浏览菜品→加入购物车→下单支付→查看订单
- 管理员登录→添加新菜品→处理订单→查看销售报表
- 每个环节的“状态变化”要展示清楚
-
重点突出技术亮点:
- 如何解决“库存超卖”?(乐观锁/分布式锁)
- 如何设计“会员积分体系”?(配置化规则)
- 如何保证“订单数据一致性”?(事务管理)
-
准备好常见问题:
- Q:为什么选Vue不选React? A:Vue学习曲线平缓,文档友好,适合快速开发
- Q:数据量大怎么办? A:菜品表按分类分库分表,订单表按月归档,加Redis缓存热门菜品
- Q:如何保证支付安全? A:敏感信息加密传输,支付接口验签,订单状态防篡改
最后:给学弟学妹的学习资料
按照这个完整流程走下来,你的餐饮管理系统毕设肯定能拿高分!如果需要更详细的技术文档、数据库设计图、或者遇到某个具体模块卡住了,都可以在评论区交流。
我还整理了一份常见问题解决方案集锦,包含了我在开发过程中遇到的20+个典型问题及其解决方法,有需要的同学可以关注后私信我领取。
记住:毕设不仅是完成任务,更是展示你综合能力的机会。把每个细节做好,把每个问题想清楚,你的努力一定会被看到!
祝大家毕设顺利,答辩一次过!如果有帮助,记得点赞收藏哦~ 🍛💻