返回 金丹・苍穹食府演法
打包后端应用
博主
大约 15 分钟
餐饮系统开发:从菜单管理到订单处理的全流程实现
餐饮系统是实战项目的经典案例,涵盖了完整的业务流程
开篇:为什么餐饮系统是很好的实战项目?
餐饮系统是一个非常适合实战的项目,它涵盖了完整的业务流程,包括菜单管理、分类管理、购物车、订单处理、支付等核心功能。通过开发餐饮系统,你可以学习到如何构建一个完整的企业级应用。
系统架构:前后端分离的技术选型
技术栈选择
后端:
- Spring Boot:快速构建后端应用
- Spring MVC:处理HTTP请求
- MyBatis-Plus:简化数据库操作
- MySQL:存储数据
- Redis:缓存和会话管理
- Spring Security:权限控制
前端:
- Vue 3:前端框架
- Element Plus:UI组件库
- Axios:HTTP客户端
- Vue Router:路由管理
- Pinia:状态管理
数据库设计:从概念模型到物理模型
核心表结构
1. 分类表(category)
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | bigint | 分类ID |
| name | varchar | 分类名称 |
| sort | int | 排序字段 |
| status | int | 状态(0禁用,1启用) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
2. 菜品表(dish)
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | bigint | 菜品ID |
| name | varchar | 菜品名称 |
| category_id | bigint | 分类ID |
| price | decimal | 价格 |
| image | varchar | 图片路径 |
| description | text | 描述 |
| status | int | 状态(0停售,1起售) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
3. 套餐表(setmeal)
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | bigint | 套餐ID |
| name | varchar | 套餐名称 |
| category_id | bigint | 分类ID |
| price | decimal | 价格 |
| image | varchar | 图片路径 |
| description | text | 描述 |
| status | int | 状态(0停售,1起售) |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
4. 套餐菜品关联表(setmeal_dish)
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | bigint | 关联ID |
| setmeal_id | bigint | 套餐ID |
| dish_id | bigint | 菜品ID |
| copies | int | 份数 |
5. 购物车表(shopping_cart)
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | bigint | 购物车ID |
| user_id | bigint | 用户ID |
| dish_id | bigint | 菜品ID |
| setmeal_id | bigint | 套餐ID |
| dish_flavor | varchar | 菜品口味 |
| number | int | 数量 |
| amount | decimal | 金额 |
| create_time | datetime | 创建时间 |
6. 订单表(orders)
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | bigint | 订单ID |
| number | varchar | 订单号 |
| status | int | 状态(1待付款,2待接单,3待派送,4已完成,5已取消) |
| user_id | bigint | 用户ID |
| address_book_id | bigint | 地址簿ID |
| order_time | datetime | 下单时间 |
| checkout_time | datetime | 结账时间 |
| pay_time | datetime | 支付时间 |
| amount | decimal | 金额 |
| remark | varchar | 备注 |
| phone | varchar | 手机号 |
| address | varchar | 地址 |
| user_name | varchar | 用户名称 |
| consignee | varchar | 收货人 |
7. 订单明细表(order_detail)
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | bigint | 明细ID |
| order_id | bigint | 订单ID |
| dish_id | bigint | 菜品ID |
| setmeal_id | bigint | 套餐ID |
| dish_flavor | varchar | 菜品口味 |
| number | int | 数量 |
| amount | decimal | 金额 |
| name | varchar | 菜品/套餐名称 |
后端开发:从API设计到业务实现
1. 分类管理
Controller
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
// 新增分类
@PostMapping
public R<String> save(@RequestBody Category category) {
categoryService.save(category);
return R.success("新增分类成功");
}
// 分类列表
@GetMapping("/list")
public R<List<Category>> list(Integer type) {
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(type != null, Category::getType, type);
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getCreateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
// 修改分类
@PutMapping
public R<String> update(@RequestBody Category category) {
categoryService.updateById(category);
return R.success("修改分类成功");
}
// 删除分类
@DeleteMapping
public R<String> delete(Long id) {
categoryService.remove(id);
return R.success("删除分类成功");
}
}
Service
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
@Override
public void remove(Long id) {
// 检查是否有关联的菜品
LambdaQueryWrapper<Dish> dishQueryWrapper = new LambdaQueryWrapper<>();
dishQueryWrapper.eq(Dish::getCategoryId, id);
int dishCount = dishService.count(dishQueryWrapper);
if (dishCount > 0) {
throw new CustomException("当前分类下存在菜品,无法删除");
}
// 检查是否有关联的套餐
LambdaQueryWrapper<Setmeal> setmealQueryWrapper = new LambdaQueryWrapper<>();
setmealQueryWrapper.eq(Setmeal::getCategoryId, id);
int setmealCount = setmealService.count(setmealQueryWrapper);
if (setmealCount > 0) {
throw new CustomException("当前分类下存在套餐,无法删除");
}
// 删除分类
super.removeById(id);
}
}
2. 菜品管理
Controller
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
// 新增菜品
@PostMapping
public R<String> save(@RequestBody DishDto dishDto) {
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
// 菜品列表
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
List<DishDto> dishDtoList = dishService.listWithFlavor(dish);
return R.success(dishDtoList);
}
// 修改菜品
@PutMapping
public R<String> update(@RequestBody DishDto dishDto) {
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}
// 批量修改状态
@PostMapping("/status/{status}")
public R<String> updateStatus(@PathVariable Integer status, Long[] ids) {
dishService.updateStatus(status, ids);
return R.success("修改状态成功");
}
// 删除菜品
@DeleteMapping
public R<String> delete(Long[] ids) {
dishService.removeWithFlavor(ids);
return R.success("删除菜品成功");
}
}
3. 购物车管理
Controller
@RestController
@RequestMapping("/shoppingCart")
@Slf4j
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
// 添加购物车
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
ShoppingCart cart = shoppingCartService.add(shoppingCart);
return R.success(cart);
}
// 购物车列表
@GetMapping("/list")
public R<List<ShoppingCart>> list() {
List<ShoppingCart> list = shoppingCartService.list();
return R.success(list);
}
// 清空购物车
@DeleteMapping("/clean")
public R<String> clean() {
shoppingCartService.clean();
return R.success("清空购物车成功");
}
}
4. 订单管理
Controller
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
// 提交订单
@PostMapping("/submit")
public R<OrderSubmitDto> submit(@RequestBody Orders orders) {
OrderSubmitDto orderSubmitDto = orderService.submit(orders);
return R.success(orderSubmitDto);
}
// 订单列表
@GetMapping("/userPage")
public R<Page<Orders>> userPage(int page, int pageSize) {
Page<Orders> pageInfo = orderService.userPage(page, pageSize);
return R.success(pageInfo);
}
// 订单详情
@GetMapping("/details/{id}")
public R<OrderDto> details(@PathVariable Long id) {
OrderDto orderDto = orderService.getDetails(id);
return R.success(orderDto);
}
// 取消订单
@PutMapping("/cancel")
public R<String> cancel(@RequestBody Orders orders) {
orderService.cancel(orders);
return R.success("取消订单成功");
}
}
前端开发:从组件设计到页面实现
1. 分类管理页面
<template> <div class="app-container"> <el-button type="primary" @click="handleAdd" size="mini" style="margin-bottom: 15px;"> <el-icon><Plus /></el-icon> 新增 </el-button> <el-table v-loading="loading" :data="categoryList" style="width: 100%"> <el-table-column type="index" width="50" label="序号" /> <el-table-column prop="name" label="分类名称" /> <el-table-column prop="sort" label="排序" /> <el-table-column prop="status" label="状态"> <template slot-scope="scope"> <el-switch v-model="scope.row.status" @change="handleStatusChange(scope.row)" /> </template> </el-table-column> <el-table-column prop="createTime" label="创建时间" width="180" /> <el-table-column label="操作" width="150" fixed="right"> <template slot-scope="scope"> <el-button @click="handleEdit(scope.row)" type="primary" size="mini"> <el-icon><Edit /></el-icon> 编辑 </el-button> <el-button @click="handleDelete(scope.row.id)" type="danger" size="mini"> <el-icon><Delete /></el-icon> 删除 </el-button> </template> </el-table-column> </el-table> <!-- 新增/编辑对话框 --> <el-dialog :title="title" v-model="dialogVisible" width="500px"> <el-form :model="form" :rules="rules" ref="form"> <el-form-item label="分类名称" prop="name"> <el-input v-model="form.name" placeholder="请输入分类名称" /> </el-form-item> <el-form-item label="排序" prop="sort"> <el-input-number v-model="form.sort" :min="0" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-switch v-model="form.status" /> </el-form-item> </el-form> <template #footer> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="submitForm">确定</el-button> </template> </el-dialog> </div> </template> <script setup> import { ref, onMounted, reactive } from 'vue' import { Plus, Edit, Delete } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' import { useRouter } from 'vue-router' import { listCategory, saveCategory, updateCategory, deleteCategory } from '@/api/category' const router = useRouter() const loading = ref(false) const dialogVisible = ref(false) const title = ref('') const form = reactive({ id: null, name: '', sort: 0, status: 1, type: 1 // 1为菜品分类,2为套餐分类 }) const rules = reactive({ name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }], sort: [{ required: true, message: '请输入排序', trigger: 'blur' }] }) const categoryList = ref([]) // 加载分类列表 const loadCategoryList = async () => { loading.value = true try { const response = await listCategory(form.type) categoryList.value = response.data } catch (error) { ElMessage.error('加载分类列表失败') } finally { loading.value = false } } // 新增分类 const handleAdd = () => { form.id = null form.name = '' form.sort = 0 form.status = 1 title.value = '新增分类' dialogVisible.value = true } // 编辑分类 const handleEdit = (row) => { form.id = row.id form.name = row.name form.sort = row.sort form.status = row.status title.value = '编辑分类' dialogVisible.value = true } // 提交表单 const submitForm = async () => { try { if (form.id) { await updateCategory(form) ElMessage.success('修改分类成功') } else { await saveCategory(form) ElMessage.success('新增分类成功') } dialogVisible.value = false loadCategoryList() } catch (error) { ElMessage.error('操作失败') } } // 删除分类 const handleDelete = async (id) => { try { await deleteCategory(id) ElMessage.success('删除分类成功') loadCategoryList() } catch (error) { ElMessage.error('删除失败:' + error.response.data.msg) } } // 修改状态 const handleStatusChange = async (row) => { try { await updateCategory(row) } catch (error) { ElMessage.error('修改状态失败') // 恢复原状态 row.status = !row.status } } // 初始化 onMounted(() => { loadCategoryList() }) </script>
2. 菜品管理页面
<template> <div class="app-container"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="菜品名称" prop="name"> <el-input v-model="queryParams.name" placeholder="请输入菜品名称" clearable @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="分类" prop="categoryId"> <el-select v-model="queryParams.categoryId" placeholder="请选择分类" clearable> <el-option v-for="category in categoryList" :key="category.id" :label="category.name" :value="category.id" /> </el-select> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="queryParams.status" placeholder="请选择状态" clearable> <el-option label="起售" value="1" /> <el-option label="停售" value="0" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" >新增</el-button> </el-col> <el-col :span="1.5"> <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multipleSelection.length === 0" @click="handleDelete" >删除</el-button> </el-col> <el-col :span="1.5"> <el-button type="warning" plain icon="el-icon-edit" size="mini" :disabled="multipleSelection.length === 0" @click="handleStatus" >批量起售/停售</el-button> </el-col> </el-row> <el-table v-loading="loading" :data="dishList" @selection-change="handleSelectionChange" border style="width: 100%" > <el-table-column type="selection" width="55" align="center" /> <el-table-column label="菜品ID" width="80" align="center" prop="id" /> <el-table-column label="菜品名称" align="center" prop="name" /> <el-table-column label="分类" align="center"> <template slot-scope="scope"> {{ getCategoryName(scope.row.categoryId) }} </template> </el-table-column> <el-table-column label="价格" align="center" prop="price" /> <el-table-column label="状态" align="center"> <template slot-scope="scope"> <el-switch v-model="scope.row.status" @change="handleStatusChange(scope.row)" /> </template> </el-table-column> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" >修改</el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row.id)" >删除</el-button> </template> </el-table-column> </el-table> <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" /> <!-- 新增/修改对话框 --> <dish-edit-dialog :visible.sync="dialogVisible" :dish="form" :title="title" @submit="submitForm" @close="dialogVisible = false" /> </div> </template> <script setup> import { ref, onMounted, computed } from 'vue' import { ElMessage } from 'element-plus' import { useRouter } from 'vue-router' import { listDish, deleteDish, updateDishStatus } from '@/api/dish' import { listCategory } from '@/api/category' import DishEditDialog from './components/DishEditDialog.vue' const router = useRouter() const loading = ref(false) const showSearch = ref(true) const total = ref(0) const dishList = ref([]) const categoryList = ref([]) const multipleSelection = ref([]) const dialogVisible = ref(false) const title = ref('') const form = ref({}) const queryParams = ref({ pageNum: 1, pageSize: 10, name: null, categoryId: null, status: null }) // 获取分类名称 const getCategoryName = (categoryId) => { const category = categoryList.value.find(c => c.id === categoryId) return category ? category.name : '' } // 加载分类列表 const loadCategoryList = async () => { try { const response = await listCategory(1) // 1为菜品分类 categoryList.value = response.data } catch (error) { ElMessage.error('加载分类失败') } } // 加载菜品列表 const getList = async () => { loading.value = true try { const response = await listDish(queryParams.value) dishList.value = response.data.records total.value = response.data.total } catch (error) { ElMessage.error('加载菜品列表失败') } finally { loading.value = false } } // 搜索 const handleQuery = () => { queryParams.value.pageNum = 1 getList() } // 重置 const resetQuery = () => { queryParams.value = { pageNum: 1, pageSize: 10, name: null, categoryId: null, status: null } handleQuery() } // 新增 const handleAdd = () => { form.value = {} dialogVisible.value = true title.value = '添加菜品' } // 修改 const handleUpdate = (row) => { form.value = { ...row } dialogVisible.value = true title.value = '修改菜品' } // 删除 const handleDelete = async (row) => { const ids = Array.isArray(row) ? row.map(item => item.id) : [row] try { await deleteDish(ids) ElMessage.success('删除菜品成功') getList() } catch (error) { ElMessage.error('删除失败') } } // 批量起售/停售 const handleStatus = () => { const ids = multipleSelection.value.map(item => item.id) const status = multipleSelection.value[0].status === 1 ? 0 : 1 updateDishStatus(status, ids).then(() => { ElMessage.success('修改状态成功') getList() }).catch(() => { ElMessage.error('修改状态失败') }) } // 修改状态 const handleStatusChange = async (row) => { try { await updateDishStatus(row.status, [row.id]) } catch (error) { ElMessage.error('修改状态失败') // 恢复原状态 row.status = !row.status } } // 选择变更 const handleSelectionChange = (val) => { multipleSelection.value = val } // 提交表单 const submitForm = () => { dialogVisible.value = false getList() } // 初始化 onMounted(() => { loadCategoryList() getList() }) </script>
项目部署:从开发到生产的完整流程
1. 后端部署
打包
# 打包后端应用
mvn clean package -DskipTests
部署到服务器
# 上传jar包到服务器
scp target/restaurant-system.jar root@server-ip:/opt/app/
# 启动应用
java -jar /opt/app/restaurant-system.jar --spring.profiles.active=prod
使用Docker部署
FROM openjdk:8-jdk-alpine VOLUME /tmp ADD target/restaurant-system.jar app.jar EXPOSE 8080 ENTRYPOINT [
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能