Spring Boot+Vue.js全栈开发:今日菜单管理系统实战教程
最近在开发一个简单的日常应用时发现很多开发者对如何设计一个轻量级的今日菜单或今日计划功能很感兴趣。这类功能看似简单但涉及数据结构设计、用户交互、数据持久化等多个环节。本文将围绕今天的饭这个主题完整拆解从需求分析到代码实现的完整流程包含前端界面、后端逻辑和数据库设计的全套方案。无论你是想学习全栈开发入门还是需要为现有项目添加日程管理功能都能从本文找到可复用的代码和设计思路。我们将使用Spring Boot Vue.js的技术栈实现一个完整的今日菜单管理系统。1. 需求分析与功能设计1.1 核心需求梳理今天的饭本质上是一个轻量级的日程管理应用主要功能包括添加今日菜单项查看今日所有菜单标记菜单完成状态删除或修改菜单项数据持久化存储1.2 功能模块拆分基于核心需求我们将系统拆分为以下模块前端界面模块负责用户交互和数据显示后端API模块处理业务逻辑和数据操作数据存储模块负责菜单数据的持久化1.3 技术选型考虑选择Spring Boot Vue.js组合的原因Spring Boot提供快速的后端开发能力内置Tomcat服务器Vue.js轻量易学适合前端初学者快速上手RESTful API设计便于前后端分离开发MySQL作为关系型数据库适合存储结构化数据2. 环境准备与项目搭建2.1 开发环境要求确保你的开发环境满足以下条件JDK 1.8或更高版本Node.js 12.0或更高版本MySQL 5.7或更高版本Maven 3.6或更高版本IDE推荐IntelliJ IDEA VS Code2.2 创建项目结构首先创建项目的整体目录结构today-meal/ ├── backend/ # Spring Boot后端项目 │ ├── src/ │ ├── pom.xml │ └── application.properties └── frontend/ # Vue.js前端项目 ├── src/ ├── package.json └── vue.config.js2.3 数据库设计创建菜单表结构CREATE DATABASE today_meal; USE today_meal; CREATE TABLE meal ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, description TEXT, meal_time VARCHAR(20) COMMENT 早餐/午餐/晚餐/加餐, completed TINYINT(1) DEFAULT 0, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );3. 后端Spring Boot实现3.1 项目依赖配置在pom.xml中添加必要依赖?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.0/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId /dependency /dependencies /project3.2 实体类设计创建Meal实体类// 文件路径backend/src/main/java/com/example/meal/entity/Meal.java package com.example.meal.entity; import javax.persistence.*; import java.time.LocalDateTime; Entity Table(name meal) public class Meal { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false, length 100) private String title; Column(columnDefinition TEXT) private String description; Column(name meal_time, length 20) private String mealTime; Column(nullable false) private Boolean completed false; Column(name created_time) private LocalDateTime createdTime; Column(name updated_time) private LocalDateTime updatedTime; // 构造方法、getter和setter省略 // 实际开发中需要使用Lombok或手动生成 }3.3 数据访问层创建Repository接口// 文件路径backend/src/main/java/com/example/meal/repository/MealRepository.java package com.example.meal.repository; import com.example.meal.entity.Meal; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface MealRepository extends JpaRepositoryMeal, Long { Query(SELECT m FROM Meal m WHERE DATE(m.createdTime) CURRENT_DATE ORDER BY m.mealTime) ListMeal findTodayMeals(); ListMeal findByCompletedFalse(); ListMeal findByMealTimeOrderByCreatedTimeDesc(String mealTime); }3.4 服务层实现创建业务逻辑服务类// 文件路径backend/src/main/java/com/example/meal/service/MealService.java package com.example.meal.service; import com.example.meal.entity.Meal; import com.example.meal.repository.MealRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; Service public class MealService { Autowired private MealRepository mealRepository; public ListMeal getTodayMeals() { return mealRepository.findTodayMeals(); } public Meal addMeal(Meal meal) { meal.setCreatedTime(LocalDateTime.now()); meal.setUpdatedTime(LocalDateTime.now()); return mealRepository.save(meal); } public Meal updateMeal(Long id, Meal mealDetails) { Meal meal mealRepository.findById(id) .orElseThrow(() - new RuntimeException(Meal not found with id: id)); meal.setTitle(mealDetails.getTitle()); meal.setDescription(mealDetails.getDescription()); meal.setMealTime(mealDetails.getMealTime()); meal.setCompleted(mealDetails.getCompleted()); meal.setUpdatedTime(LocalDateTime.now()); return mealRepository.save(meal); } public void deleteMeal(Long id) { Meal meal mealRepository.findById(id) .orElseThrow(() - new RuntimeException(Meal not found with id: id)); mealRepository.delete(meal); } public Meal toggleComplete(Long id) { Meal meal mealRepository.findById(id) .orElseThrow(() - new RuntimeException(Meal not found with id: id)); meal.setCompleted(!meal.getCompleted()); meal.setUpdatedTime(LocalDateTime.now()); return mealRepository.save(meal); } }3.5 控制器层创建REST API控制器// 文件路径backend/src/main/java/com/example/meal/controller/MealController.java package com.example.meal.controller; import com.example.meal.entity.Meal; import com.example.meal.service.MealService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/meals) CrossOrigin(origins http://localhost:8080) public class MealController { Autowired private MealService mealService; GetMapping public ListMeal getTodayMeals() { return mealService.getTodayMeals(); } PostMapping public Meal addMeal(RequestBody Meal meal) { return mealService.addMeal(meal); } PutMapping(/{id}) public Meal updateMeal(PathVariable Long id, RequestBody Meal mealDetails) { return mealService.updateMeal(id, mealDetails); } DeleteMapping(/{id}) public ResponseEntity? deleteMeal(PathVariable Long id) { mealService.deleteMeal(id); return ResponseEntity.ok().build(); } PatchMapping(/{id}/toggle) public Meal toggleComplete(PathVariable Long id) { return mealService.toggleComplete(id); } }3.6 应用配置配置application.properties# 数据库配置 spring.datasource.urljdbc:mysql://localhost:3306/today_meal?useSSLfalseserverTimezoneUTC spring.datasource.usernameroot spring.datasource.passwordyour_password # JPA配置 spring.jpa.hibernate.ddl-autoupdate spring.jpa.show-sqltrue spring.jpa.properties.hibernate.dialectorg.hibernate.dialect.MySQL8Dialect # 服务器配置 server.port80814. 前端Vue.js实现4.1 项目初始化创建Vue项目并安装必要依赖vue create frontend cd frontend npm install axios vue-router4.2 组件结构设计设计主要组件MealList.vue菜单列表显示MealForm.vue添加/编辑菜单表单MealItem.vue单个菜单项显示4.3 API服务封装创建API调用服务// 文件路径frontend/src/services/mealService.js import axios from axios; const API_BASE_URL http://localhost:8081/api/meals; const mealService { async getTodayMeals() { try { const response await axios.get(API_BASE_URL); return response.data; } catch (error) { console.error(获取今日菜单失败:, error); throw error; } }, async addMeal(mealData) { try { const response await axios.post(API_BASE_URL, mealData); return response.data; } catch (error) { console.error(添加菜单失败:, error); throw error; } }, async updateMeal(id, mealData) { try { const response await axios.put(${API_BASE_URL}/${id}, mealData); return response.data; } catch (error) { console.error(更新菜单失败:, error); throw error; } }, async deleteMeal(id) { try { await axios.delete(${API_BASE_URL}/${id}); } catch (error) { console.error(删除菜单失败:, error); throw error; } }, async toggleComplete(id) { try { const response await axios.patch(${API_BASE_URL}/${id}/toggle); return response.data; } catch (error) { console.error(切换完成状态失败:, error); throw error; } } }; export default mealService;4.4 主组件实现实现主要的Vue组件!-- 文件路径frontend/src/components/MealList.vue -- template div classmeal-list div classheader h2今天的饭/h2 button clickshowForm true classadd-btn 添加菜单/button /div div v-ifshowForm classmeal-form-container MealForm :mealeditingMeal savehandleSave cancelhandleCancel / /div div classmeal-items div v-formeal in meals :keymeal.id classmeal-item :class{ completed: meal.completed } MealItem :mealmeal edithandleEdit deletehandleDelete togglehandleToggle / /div div v-ifmeals.length 0 classempty-state 今天还没有安排菜单点击上方按钮添加吧 /div /div /div /template script import MealForm from ./MealForm.vue; import MealItem from ./MealItem.vue; import mealService from ../services/mealService; export default { name: MealList, components: { MealForm, MealItem }, data() { return { meals: [], showForm: false, editingMeal: null }; }, async created() { await this.loadMeals(); }, methods: { async loadMeals() { try { this.meals await mealService.getTodayMeals(); } catch (error) { console.error(加载菜单失败:, error); } }, handleSave(mealData) { if (this.editingMeal) { this.updateMeal(mealData); } else { this.addMeal(mealData); } this.showForm false; this.editingMeal null; }, async addMeal(mealData) { try { await mealService.addMeal(mealData); await this.loadMeals(); } catch (error) { console.error(添加菜单失败:, error); } }, async updateMeal(mealData) { try { await mealService.updateMeal(this.editingMeal.id, mealData); await this.loadMeals(); } catch (error) { console.error(更新菜单失败:, error); } }, handleEdit(meal) { this.editingMeal { ...meal }; this.showForm true; }, async handleDelete(mealId) { if (confirm(确定要删除这个菜单吗)) { try { await mealService.deleteMeal(mealId); await this.loadMeals(); } catch (error) { console.error(删除菜单失败:, error); } } }, async handleToggle(mealId) { try { await mealService.toggleComplete(mealId); await this.loadMeals(); } catch (error) { console.error(切换状态失败:, error); } }, handleCancel() { this.showForm false; this.editingMeal null; } } }; /script style scoped .meal-list { max-width: 600px; margin: 0 auto; padding: 20px; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .add-btn { background: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; } .meal-items { margin-top: 20px; } .empty-state { text-align: center; color: #666; padding: 40px; } /style4.5 菜单项组件实现单个菜单项组件!-- 文件路径frontend/src/components/MealItem.vue -- template div classmeal-item div classmeal-content div classmeal-header span classmeal-time{{ getMealTimeText(meal.mealTime) }}/span h3 classmeal-title :class{ completed: meal.completed } {{ meal.title }} /h3 /div p v-ifmeal.description classmeal-description {{ meal.description }} /p div classmeal-actions button click$emit(toggle, meal.id) classtoggle-btn {{ meal.completed ? 标记未完成 : 标记完成 }} /button button click$emit(edit, meal) classedit-btn编辑/button button click$emit(delete, meal.id) classdelete-btn删除/button /div /div /div /template script export default { name: MealItem, props: { meal: { type: Object, required: true } }, methods: { getMealTimeText(mealTime) { const timeMap { breakfast: 早餐, lunch: 午餐, dinner: 晚餐, snack: 加餐 }; return timeMap[mealTime] || mealTime; } } }; /script style scoped .meal-item { border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin-bottom: 10px; background: white; } .meal-header { display: flex; align-items: center; margin-bottom: 10px; } .meal-time { background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-right: 10px; } .meal-title { margin: 0; flex: 1; } .meal-title.completed { text-decoration: line-through; color: #888; } .meal-description { margin: 10px 0; color: #666; font-size: 14px; } .meal-actions { display: flex; gap: 10px; } .toggle-btn, .edit-btn, .delete-btn { padding: 5px 10px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; font-size: 12px; } .toggle-btn { border-color: #4CAF50; color: #4CAF50; } .edit-btn { border-color: #2196F3; color: #2196F3; } .delete-btn { border-color: #f44336; color: #f44336; } /style5. 系统部署与测试5.1 后端启动与测试启动Spring Boot应用cd backend mvn spring-boot:run使用Postman测试API接口# 获取今日菜单 GET http://localhost:8081/api/meals # 添加新菜单 POST http://localhost:8081/api/meals Content-Type: application/json { title: 番茄炒蛋, description: 家常菜简单美味, mealTime: lunch }5.2 前端启动与集成启动Vue开发服务器cd frontend npm run serve访问 http://localhost:8080 查看应用界面。5.3 功能测试流程完整的测试流程包括添加多个不同时段的菜单标记菜单完成状态编辑已有菜单信息删除不需要的菜单项验证数据持久化效果6. 常见问题与解决方案6.1 跨域问题处理如果遇到跨域问题可以在后端添加配置// 文件路径backend/src/main/java/com/example/meal/config/CorsConfig.java Configuration public class CorsConfig { Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8080) .allowedMethods(GET, POST, PUT, DELETE, PATCH); } }; } }6.2 数据库连接异常常见数据库连接问题及解决问题现象可能原因解决方案Connection refused数据库服务未启动启动MySQL服务Access denied用户名密码错误检查application.properties配置Unknown database数据库不存在创建对应的数据库6.3 前端API调用失败前端调用API常见问题排查检查后端服务是否正常启动确认API地址和端口配置正确查看浏览器控制台错误信息使用浏览器开发者工具检查网络请求7. 功能扩展与优化建议7.1 数据验证增强为实体类添加数据验证// 在Meal实体类中添加验证注解 NotBlank(message 菜单标题不能为空) Size(max 100, message 标题长度不能超过100字符) private String title;7.2 用户认证集成添加简单的用户认证// 创建用户实体和认证逻辑 Entity Table(name users) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(unique true, nullable false) private String username; // 其他字段和方法... }7.3 数据统计功能添加菜单统计功能-- 统计今日各时段菜单数量 SELECT meal_time, COUNT(*) as count FROM meal WHERE DATE(created_time) CURDATE() GROUP BY meal_time;7.4 前端体验优化优化前端用户体验添加加载状态提示实现实时搜索过滤添加菜单分类显示支持拖拽排序功能这个今天的饭管理系统虽然功能简单但涵盖了完整的前后端开发流程。通过这个项目你可以学习到Spring Boot和Vue.js的基本用法理解RESTful API设计原则掌握前后端分离开发的协作方式。在实际项目中你可以根据具体需求继续扩展功能比如添加图片上传、营养分析、购物清单等高级特性。