news 2026/9/2 3:07:54

Spring Boot 3 + Vue 3 全栈实战:手把手构建美食点餐与菜谱管理系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 3 + Vue 3 全栈实战:手把手构建美食点餐与菜谱管理系统

如果你正在寻找一个能真正体现你全栈开发能力、又能为简历和毕业设计增色的实战项目,那么一个功能完整、技术栈主流、代码规范的美食菜谱或点餐系统,无疑是当前最稳妥、最有效的选择。为什么?因为这类项目完美地融合了业务场景的普适性与技术实现的深度——它不像一个简单的“学生管理系统”那样单薄,也不像“电商秒杀系统”那样复杂到难以驾驭。它要求你从前端UI交互、后端API设计,到数据库建模、权限控制,再到部署上线,完整地走一遍现代Web应用开发的标准化流程。

然而,很多同学在动手时,往往会陷入两个误区:要么是跟着视频“照猫画虎”,代码敲完了却不知道每个配置项背后的“为什么”;要么是东拼西凑开源代码,项目结构混乱,经不起面试官的深究。最终,一个本应成为亮点的项目,变成了简历上不敢细谈的“摆设”。

本文要解决的,正是这个问题。我将带你从零开始,基于Spring Boot 3.xVue 3 (Composition API),手把手构建一个具备前后端分离架构的“美食点餐与菜谱管理系统”。我们的目标不是简单地复制代码,而是让你理解每一个技术选型背后的考量,掌握从环境搭建、功能开发、联调测试到部署上线的完整工程化思维。无论你是用于毕业设计、丰富个人简历,还是夯实全栈技能,这篇文章都将提供一条清晰、可落地的路径。文章末尾会提供完整的项目源码和详细的开发笔记。

1. 为什么选择“美食点餐系统”作为全栈实战项目?

在开始敲代码之前,我们需要先明确这个项目的价值。它绝不仅仅是一个“菜谱展示网站”。

1.1 技术栈的黄金组合:Spring Boot + Vue 3Spring Boot 是 Java 后端开发的事实标准,以其“约定大于配置”的理念极大地提升了开发效率。Vue 3 则是当前最活跃、生态最繁荣的前端框架之一,其 Composition API 提供了更灵活的逻辑复用能力。将两者结合,你实践的是业界最主流、招聘需求最旺盛的“前后端分离”架构模式。掌握这套组合,意味着你具备了应对大多数企业级Web应用开发的基础能力。

1.2 业务场景丰富,覆盖核心开发技能一个完整的点餐系统,天然包含了CRUD(增删改查)、用户认证、权限管理、数据关联、文件上传、支付集成(模拟)等核心功能点。通过实现它,你可以系统地练习到:

  • 后端:Spring MVC, Spring Data JPA / MyBatis-Plus, Spring Security, 事务管理, RESTful API设计, 全局异常处理, 参数校验, 单元测试。
  • 前端:Vue 3 组件化开发, Vue Router 路由管理, Pinia 状态管理, Axios 网络请求, Element Plus UI 库使用, 前端权限控制。
  • 工程化:Maven/Gradle 依赖管理, 跨域问题解决, 接口文档(Swagger/OpenAPI), 前后端联调, 项目打包与部署。

1.3 极高的简历辨识度与面试话题性相比于千篇一律的管理系统,一个设计精良、功能完整的点餐系统更能吸引面试官的眼球。它为你提供了大量可深入探讨的话题:

  • “你是如何设计数据库表结构来支持菜品分类、口味规格和购物车的?”
  • “前后端权限控制的粒度是如何划分的?”
  • “在高并发下单场景下,你考虑了哪些潜在问题?(如库存扣减)”
  • “前端是如何管理复杂的表单状态(如点餐车)的?”

接下来,我们将从零开始,一步步构建这个系统。

2. 项目整体架构与技术选型

在动手之前,我们先俯瞰整个项目的蓝图。

2.1 系统架构图(前后端分离)

用户浏览器 (Vue 3 App) | | (HTTP/HTTPS) v Nginx (反向代理/静态资源服务) | | (API 请求) v Spring Boot 3.x 应用服务器 | | (JDBC) v MySQL 8.0 数据库 | | (可选) v Redis (缓存/会话共享)

2.2 后端技术栈明细

  • 核心框架:Spring Boot 3.1.x (选用最新稳定版)
  • 持久层:Spring Data JPA (简化开发) 或 MyBatis-Plus (国内流行, 本文以JPA为例)
  • 安全框架:Spring Security 6.x + JWT (JSON Web Token) 实现无状态认证
  • 数据库:MySQL 8.0
  • 缓存:Spring Boot Starter Data Redis (可选,用于提升性能)
  • API文档:SpringDoc OpenAPI 3 (替代旧的Swagger2)
  • 构建工具:Maven 或 Gradle (本文使用Maven)
  • 其他:Lombok (简化代码), Hibernate Validator (参数校验), MapStruct (对象映射, 可选)

2.3 前端技术栈明细

  • 核心框架:Vue 3 (使用<script setup>语法糖)
  • 构建工具:Vite (极速的开发服务器和构建工具)
  • 路由:Vue Router 4
  • 状态管理:Pinia (Vue官方推荐的状态管理库, 替代Vuex)
  • UI组件库:Element Plus (基于Vue 3的流行UI库)
  • HTTP客户端:Axios
  • 工具库:Day.js (日期处理), Lodash-es (工具函数)

2.4 核心功能模块设计

  1. 用户模块:注册、登录(JWT)、个人信息管理。
  2. 菜品/菜谱模块:菜品分类管理、菜品信息的增删改查(含图片上传)、菜谱详情展示。
  3. 点餐/购物车模块:将菜品加入购物车、修改数量、清空购物车(前端状态管理+后端持久化)。
  4. 订单模块:提交购物车生成订单、订单状态管理(待支付、已支付、已完成等)、订单历史查询。
  5. 后台管理模块(需管理员权限):对所有数据进行综合管理。

3. 开发环境准备与项目初始化

3.1 后端环境准备

  1. JDK:确保安装 JDK 17 或更高版本(Spring Boot 3.x 要求)。
  2. IDE:IntelliJ IDEA (推荐) 或 Eclipse with STS。
  3. 数据库:安装 MySQL 8.0, 并创建一个空数据库,例如food_ordering
  4. 工具:安装 Maven 3.6+, 安装 Postman 或 Apifox 用于测试API。

使用 Spring Initializr (https://start.spring.io) 快速生成项目骨架:

  • Project: Maven
  • Language: Java
  • Spring Boot: 3.1.5 (选择最新稳定版)
  • Project Metadata:
    • Group:com.yourname
    • Artifact:food-order-backend
    • Packaging: Jar
  • Dependencies: 添加Spring Web,Spring Data JPA,MySQL Driver,Lombok,Spring Security,Validation

下载并解压, 用 IDEA 打开。

3.2 前端环境准备

  1. Node.js:安装 Node.js 18+ 和 npm。
  2. IDE:Visual Studio Code (推荐) 或 WebStorm。

使用 Vite 官方模板创建 Vue 3 项目:

# 在终端中执行 npm create vue@latest food-order-frontend

在创建过程中,根据提示选择需要的特性:TypeScript(可选但推荐),Vue RouterPiniaESLint

创建完成后,进入项目并安装 Element Plus 和 Axios:

cd food-order-frontend npm install npm install element-plus axios npm install --save-dev @element-plus/icons-vue # 图标库

4. 后端核心功能实现详解

我们聚焦于几个最具代表性的功能实现,揭示关键代码和配置。

4.1 数据库实体与JPA映射 (Entity)首先定义核心实体类,如User,Dish(菜品),Category(分类),Order,OrderItem

// 文件路径:src/main/java/com/yourname/foodorder/entity/User.java package com.yourname.foodorder.entity; import jakarta.persistence.*; import lombok.Data; import java.time.LocalDateTime; @Entity @Data @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; // 用户名 @Column(nullable = false) private String password; // 密码 (存储加密后的) private String nickname; // 昵称 private String avatar; // 头像URL @Column(unique = true) private String email; // 邮箱 private String role = "ROLE_USER"; // 角色,默认普通用户 private LocalDateTime createTime; private LocalDateTime updateTime; @PrePersist protected void onCreate() { createTime = updateTime = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updateTime = LocalDateTime.now(); } }

关键点

  • 使用@Data(Lombok) 自动生成Getter/Setter等方法。
  • @Table(name=“user”)指定表名,避免使用MySQL关键字。
  • @PrePersist@PreUpdate注解用于自动维护创建和更新时间。

4.2 Spring Security + JWT 实现认证授权这是后端安全的核心。我们将配置一个无状态的、基于JWT的认证流程。

步骤1:添加JWT依赖 (pom.xml)

<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.3</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.3</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.3</version> <scope>runtime</scope> </dependency>

步骤2:创建JWT工具类

// 文件路径:src/main/java/com/yourname/foodorder/utils/JwtUtil.java package com.yourname.foodorder.utils; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component public class JwtUtil { @Value("${jwt.secret}") // 从application.yml读取密钥 private String secret; @Value("${jwt.expiration}") // 过期时间(毫秒) private Long expiration; private SecretKey getSigningKey() { return Keys.hmacShaKeyFor(secret.getBytes()); } // 生成Token public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put("username", userDetails.getUsername()); // 可以添加更多信息,如角色 return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } // 验证并解析Token public Claims parseToken(String token) { return Jwts.parser() .verifyWith(getSigningKey()) .build() .parseSignedClaims(token) .getPayload(); } // 从Token中获取用户名 public String getUsernameFromToken(String token) { return parseToken(token).getSubject(); } // 验证Token是否过期 public Boolean isTokenExpired(String token) { Date expiration = parseToken(token).getExpiration(); return expiration.before(new Date()); } // 验证Token有效性 public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }

步骤3:配置Spring Security创建一个配置类,定义密码编码器、安全过滤链等。

// 文件路径:src/main/java/com/yourname/foodorder/config/SecurityConfig.java package com.yourname.foodorder.config; import com.yourname.foodorder.filter.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 关闭CSRF和Session .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 配置请求授权 .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**", "/api/dishes/public/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() // 公开接口 .requestMatchers("/api/admin/**").hasRole("ADMIN") // 管理员接口 .anyRequest().authenticated() // 其他所有接口需要认证 ) // 添加JWT过滤器 .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } // 配置CORS,解决前端跨域问题 @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173")); // 你的前端地址 configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }

4.3 实现菜品管理与图片上传菜品管理涉及文件上传,我们使用Spring Boot的MultipartFile处理。

// 文件路径:src/main/java/com/yourname/foodorder/controller/DishController.java package com.yourname.foodorder.controller; import com.yourname.foodorder.entity.Dish; import com.yourname.foodorder.service.DishService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @RestController @RequestMapping("/api/dishes") @RequiredArgsConstructor public class DishController { private final DishService dishService; // 创建菜品(包含图片上传) @PostMapping public ResponseEntity<Dish> createDish( @RequestParam("name") String name, @RequestParam("price") Double price, @RequestParam("categoryId") Long categoryId, @RequestParam(value = "description", required = false) String description, @RequestParam(value = "imageFile", required = false) MultipartFile imageFile) { Dish dish = dishService.createDish(name, price, categoryId, description, imageFile); return ResponseEntity.ok(dish); } // 获取所有菜品(分页查询示例) @GetMapping public ResponseEntity<List<Dish>> getAllDishes( @RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "10") int size) { // 实际应返回Page对象,此处简化 List<Dish> dishes = dishService.getAllDishes(page, size); return ResponseEntity.ok(dishes); } }

对应的Service层需要处理文件存储逻辑。通常我们会将文件存储在服务器的某个目录(如uploads/),并在数据库中保存文件的访问路径。

4.4 订单与购物车逻辑购物车数据可以暂时存储在前端(Pinia)或后端的Redis中。提交订单时,需要处理库存检查、订单总价计算、订单项生成等事务性操作。

// 文件路径:src/main/java/com/yourname/foodorder/service/impl/OrderServiceImpl.java (部分代码) @Service @Transactional @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { private final OrderRepository orderRepository; private final DishRepository dishRepository; private final OrderItemRepository orderItemRepository; @Override public Order createOrder(Long userId, List<CartItemDTO> cartItems) { // 1. 计算总价并检查库存 BigDecimal totalAmount = BigDecimal.ZERO; List<OrderItem> orderItems = new ArrayList<>(); for (CartItemDTO item : cartItems) { Dish dish = dishRepository.findById(item.getDishId()) .orElseThrow(() -> new RuntimeException("菜品不存在: " + item.getDishId())); // 检查库存(假设Dish有stock字段) if (dish.getStock() < item.getQuantity()) { throw new RuntimeException("菜品库存不足: " + dish.getName()); } // 扣减库存(悲观锁或乐观锁,生产环境需仔细设计) dish.setStock(dish.getStock() - item.getQuantity()); dishRepository.save(dish); // 构建订单项 OrderItem orderItem = new OrderItem(); orderItem.setDish(dish); orderItem.setQuantity(item.getQuantity()); orderItem.setPrice(dish.getPrice()); // 以下单时价格为准 orderItems.add(orderItem); // 累加总价 totalAmount = totalAmount.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 2. 创建订单 Order order = new Order(); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.PENDING_PAYMENT); // 待支付 order.setOrderItems(orderItems); Order savedOrder = orderRepository.save(order); // 3. 保存订单项并关联订单 for (OrderItem item : orderItems) { item.setOrder(savedOrder); orderItemRepository.save(item); } return savedOrder; } }

关键点@Transactional注解确保了创建订单、扣减库存、保存订单项这几个数据库操作在一个事务中,要么全部成功,要么全部回滚。

5. 前端核心功能实现与联调

前端我们使用 Vue 3 + Pinia + Element Plus 来构建用户界面。

5.1 配置Axios与全局状态管理 (Pinia)首先,创建一个带有请求拦截器的Axios实例,用于自动添加JWT Token。

// 文件路径:src/utils/request.js import axios from 'axios'; import { ElMessage } from 'element-plus'; import { useUserStore } from '@/stores/user'; // Pinia store const service = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, // 从.env文件读取 timeout: 10000, }); // 请求拦截器 service.interceptors.request.use( (config) => { const userStore = useUserStore(); if (userStore.token) { config.headers['Authorization'] = `Bearer ${userStore.token}`; } return config; }, (error) => { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( (response) => { const res = response.data; // 根据你的后端统一响应格式处理,例如 { code: 200, data: {}, message: 'success' } if (res.code !== 200) { ElMessage.error(res.message || 'Error'); return Promise.reject(new Error(res.message || 'Error')); } else { return res.data; // 直接返回业务数据 } }, (error) => { // 处理HTTP错误状态码,如401未授权 if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录'); // 清空用户信息,跳转到登录页 const userStore = useUserStore(); userStore.logout(); window.location.href = '/login'; } else { ElMessage.error(error.message || '请求失败'); } return Promise.reject(error); } ); export default service;

5.2 实现登录与Token管理 (Pinia Store)

// 文件路径:src/stores/user.js import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { login as apiLogin, getUserInfo } from '@/api/auth'; // 引入API函数 import router from '@/router'; export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || ''); const userInfo = ref(JSON.parse(localStorage.getItem('userInfo')) || {}); const isLoggedIn = computed(() => !!token.value); // 登录动作 const login = async (loginForm) => { try { const data = await apiLogin(loginForm); token.value = data.token; userInfo.value = data.userInfo || {}; // 持久化存储 localStorage.setItem('token', token.value); localStorage.setItem('userInfo', JSON.stringify(userInfo.value)); // 跳转到首页 router.push('/'); ElMessage.success('登录成功'); } catch (error) { throw error; // 由调用方处理错误 } }; // 登出动作 const logout = () => { token.value = ''; userInfo.value = {}; localStorage.removeItem('token'); localStorage.removeItem('userInfo'); router.push('/login'); }; // 获取用户信息(用于页面刷新后恢复状态) const fetchUserInfo = async () => { if (!token.value) return; try { const info = await getUserInfo(); userInfo.value = info; localStorage.setItem('userInfo', JSON.stringify(info)); } catch (error) { logout(); // 获取失败,可能是token失效,直接登出 } }; return { token, userInfo, isLoggedIn, login, logout, fetchUserInfo, }; });

5.3 菜品列表与购物车组件示例这是一个使用 Composition API 的菜品列表组件,包含加入购物车功能。

<!-- 文件路径:src/views/DishListView.vue --> <template> <div class="dish-list"> <el-row :gutter="20"> <el-col :span="6" v-for="dish in dishList" :key="dish.id"> <el-card :body-style="{ padding: '0px' }" class="dish-card"> <img :src="dish.imageUrl || '/placeholder.png'" class="dish-image" /> <div style="padding: 14px;"> <h3>{{ dish.name }}</h3> <p class="description">{{ dish.description }}</p> <div class="bottom"> <span class="price">¥{{ dish.price }}</span> <el-button type="primary" size="small" @click="addToCart(dish)"> 加入购物车 </el-button> </div> </div> </el-card> </el-col> </el-row> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import { ElMessage } from 'element-plus'; import { getDishList } from '@/api/dish'; import { useCartStore } from '@/stores/cart'; // 购物车状态管理 const dishList = ref([]); const cartStore = useCartStore(); // 获取菜品列表 const loadDishes = async () => { try { const data = await getDishList({ page: 1, size: 12 }); dishList.value = data; } catch (error) { ElMessage.error('加载菜品失败'); } }; // 加入购物车 const addToCart = (dish) => { cartStore.addItem({ id: dish.id, name: dish.name, price: dish.price, image: dish.imageUrl, quantity: 1, }); ElMessage.success(`已添加 ${dish.name} 到购物车`); }; onMounted(() => { loadDishes(); }); </script> <style scoped> .dish-card { margin-bottom: 20px; } .dish-image { width: 100%; height: 180px; object-fit: cover; } .bottom { margin-top: 13px; display: flex; justify-content: space-between; align-items: center; } .price { color: #f56c6c; font-size: 18px; font-weight: bold; } .description { color: #909399; font-size: 12px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } </style>

6. 项目运行与效果验证

6.1 后端启动与验证

  1. 确保MySQL服务已启动,并创建了数据库food_ordering
  2. application.yml中配置数据库连接和JWT密钥。
# src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/food_ordering?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 首次启动用update自动建表,生产环境用validate或none show-sql: true properties: hibernate: format_sql: true jwt: secret: yourSuperSecretKeyHereAtLeast32BytesLongForHS256 expiration: 86400000 # 24小时,单位毫秒 logging: level: com.yourname.foodorder: DEBUG
  1. 在IDEA中运行主类FoodOrderBackendApplication
  2. 访问http://localhost:8080/swagger-ui/index.html, 查看自动生成的API文档,并测试注册、登录接口。

6.2 前端启动与联调

  1. food-order-frontend目录下,创建.env.development文件,配置后端API地址。
VITE_APP_BASE_API=http://localhost:8080/api
  1. 运行开发服务器。
npm run dev
  1. 访问http://localhost:5173, 你应该能看到前端页面。
  2. 尝试注册新用户、登录、浏览菜品、将菜品加入购物车、提交订单。通过浏览器开发者工具的Network面板,观察前后端API的请求与响应。

7. 常见问题与排查思路

在开发过程中,你几乎一定会遇到以下问题。这里提供快速排查思路。

问题现象可能原因排查方式解决方案
前端访问后端API出现CORS错误后端未正确配置跨域查看浏览器Console错误信息,确认是OPTIONS预检请求失败还是请求被拒。检查后端SecurityConfig中的corsConfigurationSource()配置,确保允许了前端的源、方法和头信息。
登录成功,但后续请求返回4011. Token未正确附加到请求头。
2. Token已过期。
3. 后端JWT密钥不一致。
1. 检查前端Axios拦截器,查看请求头是否有Authorization: Bearer <token>
2. 检查Token过期时间。
3. 检查后端启动时JWT密钥是否一致。
1. 确保拦截器逻辑正确,从Store中获取了token。
2. 前端捕获401错误,跳转登录页。
3. 确保生产环境JWT密钥固定,不要每次重启变化。
数据库表没有自动创建1.ddl-auto配置为nonevalidate
2. 数据库连接失败。
3. 实体类注解有误。
1. 检查application.ymlspring.jpa.hibernate.ddl-auto值。
2. 查看启动日志,是否有数据库连接错误。
3. 检查实体类@Entity,@Id等注解。
1. 开发环境可暂时设为update,但生产环境必须改为validatenone,并使用SQL脚本管理表结构。
2. 检查数据库地址、用户名、密码。
3. 修正实体类。
文件上传失败或找不到1. 上传目录不存在或没有写权限。
2. 文件大小超出Spring Boot默认限制。
3. 前端未正确构造FormData
1. 查看后端日志,是否有FileNotFoundException或权限错误。
2. 检查是否报MaxUploadSizeExceededException
3. 检查前端请求的Content-Type是否为multipart/form-data
1. 确保服务器上传目录存在且可写。
2. 在application.yml中配置spring.servlet.multipart.max-file-sizemax-request-size
3. 前端使用new FormData()并正确append字段。
Vue页面刷新后状态丢失Pinia状态是内存中的,刷新页面会重置。检查登录后,token和用户信息是否已持久化到localStoragesessionStorage在Pinia Store的登录动作中,将必要信息存入localStorage。在Store初始化时,从localStorage读取并恢复状态。

8. 项目优化与进阶实践(最佳实践)

一个能写进简历的毕业设计,不能只停留在“跑通”层面。以下优化方向能显著提升你的项目质量。

8.1 安全性加固

  • 密码加密:必须使用BCryptPasswordEncoder等强哈希算法,切勿明文存储。
  • SQL注入防护:使用JPA或MyBatis的参数化查询,从根本上杜绝。
  • XSS防护:后端对用户输入进行过滤或转义,前端框架(如Vue)本身有一定防护,但渲染富文本时需谨慎。
  • CSRF防护:虽然RESTful API+JWT无状态架构对CSRF免疫,但了解其原理仍是面试加分项。
  • API速率限制:使用Spring Boot Starter ActuatorResilience4jSentinel对关键接口(如登录、下单)做限流,防止恶意请求。

8.2 性能优化

  • 数据库索引:为高频查询字段(如user.username,dish.category_id,order.user_id)添加索引。
  • 缓存应用:使用Redis缓存热点数据,如菜品分类、热门菜品信息。使用@Cacheable注解简化开发。
  • 图片资源优化:上传图片时生成缩略图;使用CDN或对象存储(如OSS、COS)存放静态资源,减轻服务器压力。
  • 前端懒加载与分页:菜品列表、订单历史等使用分页查询,避免一次性加载过多数据。Vue Router 支持路由懒加载。

8.3 代码质量与可维护性

  • 统一响应封装:定义如Result<T>的通用响应类,包含code,data,message字段。
  • 全局异常处理:使用@ControllerAdvice@ExceptionHandler捕获并处理各类异常,返回友好的错误信息。
  • 参数校验:在Controller方法参数或DTO上使用@Validated@NotBlank,@Size等注解进行校验。
  • 使用DTO进行层间数据传输:避免直接暴露Entity给前端,使用MapStructModelMapper进行对象转换。
  • 编写单元测试:对Service层核心业务逻辑(如创建订单、扣库存)编写JUnit单元测试,保证代码质量。

8.4 部署上线

  • 后端打包:使用mvn clean package -DskipTests生成可执行的jar文件。
  • 前端构建:运行npm run build生成静态文件到dist目录。
  • 环境分离:准备application-dev.yml,application-prod.yml配置文件,通过spring.profiles.active激活。
  • 使用Docker容器化:编写Dockerfiledocker-compose.yml,一键部署应用、数据库和Redis,这是现代运维的必备技能。
  • 选择云服务:可以考虑在阿里云、腾讯云等平台购买轻量应用服务器,实践从代码到线上服务的完整流程。

通过完成这个项目,你不仅得到了一个功能完整的“美食点餐系统”源码,更重要的是,你经历了一个标准的企业级Web应用从设计到实现的完整生命周期。在面试中,你可以自信地展示这个项目,并深入讨论任何一个技术细节。建议你将项目代码托管到GitHub,并撰写清晰的README,这本身就是你技术能力的最好证明。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 3:07:36

DeepSeek英转中字幕翻译全流程:从SRT解析到API批量调用

最近在折腾老番字幕资源时&#xff0c;很多做“老番整理”或“外挂字幕归档”的同学应该都有同感&#xff1a;上世纪 90 年代的 OVA 动画&#xff0c;网上能轻松找到的往往是英文字幕&#xff0c;中文字幕要么缺轴、要么机翻味太重&#xff0c;根本没法舒服地看下去。标题里的《…

作者头像 李华
网站建设 2026/9/2 3:07:22

水质参数反演分析系统技术拆解:从遥感影像到水质分布图

传统水质监测的流程&#xff0c;很多人第一反应是“采样&#xff0c;送回实验室&#xff0c;等结果”。这套流程在常规管理中没有问题&#xff0c;但当你想知道一个大型水库、一条跨区域河流、或者雨季洪水过后的整片河网的水质分布时&#xff0c;实验室采样几乎无法回答。点位…

作者头像 李华
网站建设 2026/9/2 3:06:18

图工程自嗨避坑指南:从Neo4j到Graph RAG的价值落地

在技术社区里&#xff0c;经常能看到一个很有意思的场景&#xff1a;团队花了几周时间搭起一张漂亮的图&#xff0c;节点上万、关系上万、可视化一打开满屏连线&#xff0c;汇报时很有冲击力。但业务方看完只问了一句&#xff1a;“所以呢&#xff1f;它能帮我解决什么问题&…

作者头像 李华
网站建设 2026/9/2 3:06:17

JDK 1.8.0_201官方正式版下载安装与多版本共存配置指南

简介&#xff1a;这份 JDK 1.8.0_201 为官方正式版在 Windows x64 下的免安装绿色包&#xff0c;面向 Java 初学者与需要快速搭建 JDK 8 环境的开发者&#xff0c;省去安装向导和系统变量配置的繁琐步骤&#xff0c;解压后即可用于编译、运行与调试 Java 程序。压缩包采用 7z 格…

作者头像 李华
网站建设 2026/9/2 3:06:15

技术博客选题怎么做?CSDN高质量文章主题推荐与避坑指南

抱歉&#xff0c;这个输入主题无法用于生成 CSDN 技术博客文章。 原因是&#xff1a;您输入的标题是一段偶像演出直拍视频的标题&#xff08;“李宣美 4K 竖屏直拍”&#xff09;&#xff0c;内容属于娱乐资讯或演唱会物料&#xff0c;不是技术项目、开发工具、编程框架、数据…

作者头像 李华
网站建设 2026/9/2 3:06:00

DeepSeek英转中字幕实战:从SRT解析到术语表与API部署全指南

硬盘里翻出一部 1995 年的老 OVA&#xff0c;画风复古&#xff0c;音轨倒是完整&#xff0c;但手头只有一条英文字幕。想转发给朋友&#xff0c;对方说“看不懂英文”&#xff0c;于是你打算自己做一版中文字幕。手动逐句翻译太慢&#xff0c;机器翻译又经常把上下文搞丢。你听…

作者头像 李华