返回 金丹・苍穹食府演法

打包后端应用

博主
大约 15 分钟

餐饮系统开发:从菜单管理到订单处理的全流程实现

餐饮系统是实战项目的经典案例,涵盖了完整的业务流程

开篇:为什么餐饮系统是很好的实战项目?

餐饮系统是一个非常适合实战的项目,它涵盖了完整的业务流程,包括菜单管理、分类管理、购物车、订单处理、支付等核心功能。通过开发餐饮系统,你可以学习到如何构建一个完整的企业级应用。

餐饮系统

系统架构:前后端分离的技术选型

技术栈选择

后端:

  • Spring Boot:快速构建后端应用
  • Spring MVC:处理HTTP请求
  • MyBatis-Plus:简化数据库操作
  • MySQL:存储数据
  • Redis:缓存和会话管理
  • Spring Security:权限控制

前端:

  • Vue 3:前端框架
  • Element Plus:UI组件库
  • Axios:HTTP客户端
  • Vue Router:路由管理
  • Pinia:状态管理

数据库设计:从概念模型到物理模型

核心表结构

1. 分类表(category)

字段名数据类型描述
idbigint分类ID
namevarchar分类名称
sortint排序字段
statusint状态(0禁用,1启用)
create_timedatetime创建时间
update_timedatetime更新时间

2. 菜品表(dish)

字段名数据类型描述
idbigint菜品ID
namevarchar菜品名称
category_idbigint分类ID
pricedecimal价格
imagevarchar图片路径
descriptiontext描述
statusint状态(0停售,1起售)
create_timedatetime创建时间
update_timedatetime更新时间

3. 套餐表(setmeal)

字段名数据类型描述
idbigint套餐ID
namevarchar套餐名称
category_idbigint分类ID
pricedecimal价格
imagevarchar图片路径
descriptiontext描述
statusint状态(0停售,1起售)
create_timedatetime创建时间
update_timedatetime更新时间

4. 套餐菜品关联表(setmeal_dish)

字段名数据类型描述
idbigint关联ID
setmeal_idbigint套餐ID
dish_idbigint菜品ID
copiesint份数

5. 购物车表(shopping_cart)

字段名数据类型描述
idbigint购物车ID
user_idbigint用户ID
dish_idbigint菜品ID
setmeal_idbigint套餐ID
dish_flavorvarchar菜品口味
numberint数量
amountdecimal金额
create_timedatetime创建时间

6. 订单表(orders)

字段名数据类型描述
idbigint订单ID
numbervarchar订单号
statusint状态(1待付款,2待接单,3待派送,4已完成,5已取消)
user_idbigint用户ID
address_book_idbigint地址簿ID
order_timedatetime下单时间
checkout_timedatetime结账时间
pay_timedatetime支付时间
amountdecimal金额
remarkvarchar备注
phonevarchar手机号
addressvarchar地址
user_namevarchar用户名称
consigneevarchar收货人

7. 订单明细表(order_detail)

字段名数据类型描述
idbigint明细ID
order_idbigint订单ID
dish_idbigint菜品ID
setmeal_idbigint套餐ID
dish_flavorvarchar菜品口味
numberint数量
amountdecimal金额
namevarchar菜品/套餐名称

后端开发:从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 功能