news 2026/9/26 15:47:16

SpringBoot+Vue在线点餐系统:毕业设计源码解析与部署指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue在线点餐系统:毕业设计源码解析与部署指南

简介:基于Springboot和Vue的在线点餐系统源码与数据库,是一份面向计算机相关专业毕业设计的高质量项目,也适合期末课程设计、课程大作业等场景。资源包含完整的后端逻辑、前端页面以及数据库脚本,涉及用户点餐、订单管理、菜品分类、后台管理等常见业务模块,能够帮助开发者梳理前后端分离架构下的系统实现思路。压缩包共包含472个文件,涉及Java后端源码、XML配置、HTML页面、JavaScript脚本、CSS样式、图片素材以及可直接导入的SQL数据库脚本,并附带项目说明文档;包体大小为19.67MB,目录结构清晰,便于按模块定位和学习。该项目为个人原创毕设,评审分超过95分,代码经过严格调试确保可运行,目前已有185人学习,是系统学习Springboot与Vue整合开发、数据库设计以及毕业设计选题落地的可靠参考。

1. 这个标题意味着什么:一份拿到手就能跑的毕设全家桶

“基于Springboot+Vue的在线点餐系统源码+数据库(毕业设计).zip”这个命名,几乎是国内计算机类毕业设计里最经典的一类交付物。它把前端、后端、数据库三样东西打包在一起,解决了学生从零造轮子最大的痛点:环境搭不起来、项目跑不通、论文没素材。这套系统的业务本质是电商的缩小版,菜品列表相当于商品橱窗,购物车对应购物流程,订单表承载状态流转,后台管理页负责数据维护。能解决什么?一是直接用现成的源码和 SQL 脚本快速本地跑通,二是借这个完整闭环理解前后端分离项目到底怎么组织代码,三是为答辩准备一条清晰的业务主线。适合正在做毕业设计、短期冲刺课程设计、或者想补一个全栈作品的初级开发者。

2. 先拆压缩包:系统架构、目录结构与数据库设计

2.1 为什么 Spring Boot + Vue 能成为毕业设计的主流组合

这个标题把“Springboot + Vue”写在项目名里,不是偶然。Spring Boot 自带嵌入式 Tomcat,打包成一个 jar 就能跑,不用像老 SSM 项目那样在 Tomcat 里反复配置数据源和字符集,这对学生来说省掉了一大半环境问题。Vue 则把前端页面拆成组件,登录页、菜品列表、购物车、后台管理各占一个组件,代码结构比传统 JSP 清晰太多,也更容易在论文里画架构图。

我拿到这类项目源码时,第一件事是看它的分层是否标准。常见的做法是后端按controller → service → mapper三层拆包,前端按views → components → router → api组织页面。只要分层在,哪怕代码有瑕疵,接手成本也不高。要是哪个源码把几百行逻辑全塞在 Controller 里,跑起来再流畅我都建议别选,因为答辩时老师一眼就能看出设计能力不够。

这套系统的运行拓扑通常是:浏览器访问 Vue 打包后的静态资源,由 Nginx 或开发服务器托管;前端通过 HTTP 请求访问 Spring Boot 提供的 RESTful 接口;Spring Boot 通过 MyBatis 操作 MySQL 数据库。前端和后端之间通过 JSON 交换数据,身份认证依赖 Token,而不是传统 Session。理解这个链路,后面所有调试都有方向。

2.2 解压后常见的文件组织方式

这类毕设压缩包虽然命名各异,但内部结构基本一致。后端是一个 Maven 工程,包含pom.xml和src目录;前端是一个用 Vue CLI 创建的前端工程,包含package.json和src目录;另外通常有一个独立的sql或db文件夹,放数据库初始化脚本。如果压缩包里还带了 README 或部署文档,那说明作者比较负责,优先读它。

我一般会先确认三份文件再动手:

文件作用关注点
pom.xml后端依赖清单Spring Boot 版本、MyBatis 版本、MySQL 驱动
application.yml或.properties后端配置数据库连接、端口、日志级别、JWT 密钥
package.json前端依赖清单Vue 版本、Element UI 版本、axios 版本

这三个文件能透露大量信息。比如 Spring Boot 2.x 用的是javax.servlet包,Spring Boot 3.x 用的是jakarta.servlet,二者写法不同,网上很多报错教程对不上号就是因为版本差了一代。Vue 2 和 Vue 3 的路由语法也有差异,router.beforeEach在 3.x 里返回 Promise,在 2.x 里直接用next()。先把版本确认清楚,再搜具体报错,才不会浪费时间。

2.3 数据库设计:点餐系统的表结构拆解

在线点餐的核心数据模型绕不开这几张表:用户表、菜品分类表、菜品表、购物车表、订单表、订单明细表、地址表。它们是整个系统的地基,也是答辩时老师最容易追问的地方。

用户表一般包含id、username、password、phone、role等字段。role字段区分普通用户和管理员,管理员可以进后台管理菜品和订单。这里的密码通常用 MD5 加盐或 BCrypt 存储,直接存明文会显得很不专业。菜品分类表和菜品表是一对多关系,菜品表里的category_id作为外键指向分类表。

订单表是整张数据模型的核心,我贴一份常见结构的建表语句,字段命名可以按项目实际调整:

CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号,用于展示和查询', `user_id` bigint NOT NULL COMMENT '下单用户ID', `address_id` bigint DEFAULT NULL COMMENT '配送地址ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0待支付,1已支付,2配送中,3已完成,4已取消', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

order_no加唯一索引是必须的,它相当于订单的身份证,用户查询订单、对接支付回调、后台对账都用它。status用tinyint存数字状态值而不是字符串,是为了查询和索引效率更高,前端显示时再映射成中文文案。create_time和update_time带上默认值,代码里就不用每次手动填时间,这个习惯在答辩里提一下很加分。

订单明细表orders_detail记录每个订单买了哪些菜品,包含order_no、dish_id、dish_name、price、quantity字段。之所以要冗余一份dish_name和price,是因为菜品价格改动了,历史订单也要保留下单那一刻的快照。很多新手只存dish_id,结果改完菜品价格后,历史订单金额对不上,这就是没做数据冗余的翻车现场。

购物车表相对简单,user_id、dish_id、quantity三个核心字段,再加一个create_time就够了。用户加购时先按user_id + dish_id查一下,存在就更新数量,不存在就插入新记录,这样能避免出现同一道菜在购物车里有两条重复数据。地址表则记录用户的收货信息,包含收货人、电话、详细地址、默认标志位。

3. Spring Boot 后端核心实现:认证、下订单与状态流转

3.1 JWT 登录认证:用 Token 替代 Session 的完整链路

毕设系统如果做手机端和后台管理共用一套接口,用 Session 保存登录态会很别扭。常见做法是用 JWT 生成 Token,后端不存登录状态,每次请求由拦截器校验 Token 的合法性和有效期,这也是现代前后端分离项目的主流方案。

登录接口的逻辑很直接:接收用户名和密码,校验通过后生成 Token 返回前端。核心代码大致是这样:

public LoginResponse login(LoginRequest request) { // 1. 根据用户名查用户 User user = userMapper.selectOne( new LambdaQueryWrapper<User>() .eq(User::getUsername, request.getUsername())); // 2. 校验密码,实际项目使用 BCrypt 比对密文 if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BusinessException("用户名或密码错误"); } // 3. 生成 JWT,payload 里只放用户ID和角色 String token = JwtUtil.createToken(user.getId(), user.getRole()); // 4. 返回用户信息和 Token,Vue 端存到 localStorage return new LoginResponse(token, user.getUsername(), user.getRole()); }

这里的JwtUtil是工具类,负责用密钥对用户信息做签名。我只在 Token 里放userId和role,不放大段用户信息,一方面是为了控制 Token 体积,另一方面避免敏感信息被解码看到。真正取用户详情时,后端根据userId再查一次数据库。

有了 Token,还需要一个拦截器统一校验。拦截器里排除掉登录接口、注册接口和菜品浏览接口,其余接口全部要求携带合法的Authorization请求头:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 预检请求直接放行,否则前端跨域请求会被拦 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException("未登录或Token已过期"); } // 解析Token,把用户ID放到request作用域,后续Controller直接取 Long userId = JwtUtil.parseToken(token.substring(7)); request.setAttribute("userId", userId); return true; } }

拦截器里最关键的是放过OPTIONS预检请求。前后端分离部署时,浏览器发送跨域请求前会先发一次OPTIONS预检,如果不放行,前端会看到“请求成功但拿不到数据”的怪现象。这个坑我在后面避坑章节里会再展开一次,因为十个人里有八个人会踩。

3.2 订单状态机:从购物车到支付完成的流转逻辑

点餐系统的订单生命周期通常包含四个核心状态:待支付、已支付、配送中、已完成。再加一个已取消作为异常兜底。设计上用数字枚举维护状态,而不是随意写字符串,这样代码里能统一控制状态迁移的合法性。

我自己在处理这类业务时,会写一个订单状态流转的服务方法,每次状态变更都校验前置状态,避免出现“已完成订单被改成待支付”这种脏数据:

@Transactional(rollbackFor = Exception.class) public void updateOrderStatus(String orderNo, Integer targetStatus) { Order order = orderMapper.selectOne( new LambdaQueryWrapper<Order>() .eq(Order::getOrderNo, orderNo)); if (order == null) { throw new BusinessException("订单不存在"); } // 状态机校验:只允许相邻状态迁移 Integer current = order.getStatus(); if (!canTransit(current, targetStatus)) { throw new BusinessException("非法的订单状态变更"); } Order update = new Order(); update.setId(order.getId()); update.setStatus(targetStatus); orderMapper.updateById(update); } private boolean canTransit(Integer current, Integer target) { // 0待支付 -> 1已支付 -> 2配送中 -> 3已完成 if (current == 0 && target == 1) return true; if (current == 1 && target == 2) return true; if (current == 2 && target == 3) return true; // 待支付和配送中都允许取消 if ((current == 0 || current == 1) && target == 4) return true; return false; }

@Transactional注解必须加,因为修改订单状态通常伴随其他写操作,比如清空购物车、扣减库存,任何一个失败都要整体回滚。状态机的好处是逻辑集中,天知道没有这层校验时,脏数据会以什么形式溜进数据库。

下单接口是事务的另一个典型场景。用户提交订单时,后端同时要插入订单表和订单明细表,还要清空购物车。这里我用订单号orderNo做关联,而不是数据库自增 ID,因为订单明细要先知道订单号才能插入。前端传购物车列表过来,后端统一计算金额,这样做比信任前端传的金额靠谱得多,前端传的总金额只能作参考。

3.3 MyBatis-Plus 分页查询:后台菜品列表的常规写法

管理端菜品列表通常要做分页查询和按名称模糊搜索。MyBatis-Plus 里的分页插件配上 LambdaQueryWrapper,代码量比手写 XML 少一半。

public IPage<Dish> queryDishPage(Integer pageNum, Integer pageSize, String name) { Page<Dish> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), Dish::getName, name) .orderByDesc(Dish::getCreateTime); return dishMapper.selectPage(page, wrapper); }

wrapper.like的第一个条件参数为 false 时不拼接该条件,这就避免了手动拼 SQL 时常见的空串判断问题。分页插件需要在配置类里注册MybatisPlusInterceptor,网上教程各有差异,关键是确认pom.xml里 MyBatis-Plus 版本与 Spring Boot 版本兼容。早期版本用PaginationInterceptor,3.4 之后改成了MybatisPlusInterceptor,照老教程配置新版本会直接启动报错。

需要注意的是,前端管理表格用的是 Element UI 的el-table,它期望后端返回{ records: [], total: 100 }这种结构。如果后端直接返回IPage对象,字段名是records和total,通常正好对得上。但也有项目封装了统一返回体Result<T>,把IPage塞进data字段里,这时候前端取值路径就变了。我习惯在后端做一个 PageResult 封装层,把分页数据统一包装,前端不管后端内部怎么实现,拿到的结构永远稳定。

4. Vue 前端核心:路由守卫、请求封装与购物车页面逻辑

4.1 路由设计与登录守卫:没登录就跳转登录页

前端项目拿过来,先看src/router目录下的路由配置。在线点餐系统的路由一般分成两组:面向用户的前台页面(菜品浏览、购物车、我的订单)和面向管理员的后台页面(菜品管理、订单处理、分类管理)。两层页面需要不同的权限控制,普通用户不该进后台,管理员也没必要逛购物车。

Vue Router 的全局前置守卫是控制访问权限的标准做法:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') // 路由元信息里标记需要登录的页面 if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) return } // 后台页面额外要求管理员角色 if (to.meta.requiresAdmin) { const role = localStorage.getItem('role') if (role !== 'admin') { next({ path: '/', query: { noAuth: true } }) return } } next() })

登录后跳回原页面的细节别忽略。用户被踢到登录页时,我用query.redirect记下了原本要访问的地址,登录成功再跳回去,这个体验很拉好感。meta字段是路由配置里最容易被新手忽略的部分,它让权限声明跟路由定义放在一起,比在组件里各自判断清晰得多。

Vue 3 的项目还要注意next()的用法变了。Vue Router 4 里如果to和from都是登录页,不调用next()而直接return true或return false也能实现控制。老教程里大量next()写法的示例在 3.x 项目里会触发警告,遇到这种情况不用慌,按当前项目的 Vue 版本来。

4.2 axios 请求封装:Token 注入与 401 统一处理

前端每个页面都要发请求,如果每次都手动从 localStorage 取 Token 塞进 header,代码会非常啰嗦,而且 Token 过期时每个接口都要单独处理。标准做法是在src/api/request.js里封装一个 axios 实例,请求拦截器统一注入 Token,响应拦截器统一处理错误码。

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带Token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理业务错误和登录超时 request.interceptors.response.use( response => { const res = response.data // 后端统一返回 { code, msg, data } 结构 if (res.code !== 200) { // 弹出错误提示 ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') localStorage.removeItem('role') // 跳转到登录页,附带当前页面地址便于回跳 window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname) } ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default request

后端返回体的code字段里的业务错误(比如库存不足、订单状态非法)和 HTTP 状态码是两回事,很多新手只处理了后者。这里的思路是:HTTP 状态码 200 但业务失败的情况,在后端code不为 200 时统一弹错误提示;HTTP 状态码 401 时统一清掉本地登录态。这样前端业务代码里不需要到处写if (res.code !== 200)的判断。

4.3 购物车与下单:用户页面的两个核心交互

购物车页面的逻辑其实不复杂,难在状态同步。用户改数量、删菜品、勾选菜品,每一步都影响最终总价和提交参数。我在做这类页面时会用一个计算属性来汇总“已勾选菜品”,这样任何数据变化后,总价自动跟着变,不需要手动调用更新函数:

const cartList = ref([]) const selectedDishes = computed(() => cartList.value.filter(item => item.checked) ) const totalPrice = computed(() => selectedDishes.value.reduce( (sum, item) => sum + item.price * item.quantity, 0 ) )

提交订单时,前端只把“已选中的菜品 ID 和数量”发给后端,金额由后端重新计算,这能防止用户篡改请求金额。这一段是答辩时很好的提问点,老师问“前端传的金额能不能信”时,能答出“不信、后端重算”就说明真的做过项目。

下单成功后的页面反馈同样有讲究。后端返回带orderNo的订单数据后,前端跳转到订单详情页,而不是弹个“下单成功”就完事。用户需要看到订单号、菜品清单、金额、配送状态,这些信息都从后端重新拉取,不用本地缓存,防止刷新后数据丢失。前端这一整条链路跑通,整个项目的完成度立刻上一个台阶。

5. 从零跑通:数据库导入、配置修改与本地启动

5.1 初始化数据库与修改 Spring Boot 配置

拿到压缩包后第一步不是启动后端,而是先把数据库准备好。打开 MySQL,新建一个数据库,然后把压缩包里sql目录下的脚本导入。我在终端里的做法是:

mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS ordering_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -u root -p ordering_system < /path/to/ordering_system.sql

数据库字符集必须用utf8mb4。项目里头像、菜品描述、备注信息很可能带 Emoji 字符,utf8mb3存不下 4 字节的符号,导入脚本或插入数据时会报Incorrect string value错误。这一步设对了,后面能少踩一半乱码坑。

接着改后端配置文件application.yml。常见做法是配置数据库连接、端口和日志:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ordering_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

serverTimezone=Asia/Shanghai一定要加,不然新版 MySQL 驱动会拿服务器默认时区跟本地对不上,报错提示很隐晦。log-impl改成StdOutImpl后,控制台能直接看到 MyBatis 执行的 SQL 语句,排查“接口返回数据不对”问题时靠它看实际查询条件,比肉眼比对实体类快得多。

如果你的 MySQL 是 5.7 版本,driver-class-name用com.mysql.jdbc.Driver也可以,但新版 MySQL 8.x 驱动推荐用com.mysql.cj.jdbc.Driver。项目里驱动类跟数据库版本不匹配时,启动通常会报ClassNotFoundException或者连接超时。

5.2 后端与前端启动命令:Maven 和 npm 的完整流程

后端启动前建议先确认 Maven 有没有正确配置国内镜像。pom.xml里依赖如果下载不动,大概率是连 Maven 中央仓库太慢。在 Maven 的settings.xml里配置阿里云镜像就能解决,这也是国内开发者最常见的高频操作。

后端启动的命令很简单,在含pom.xml的目录下执行:

mvn spring-boot:run

第一次启动要下载大量依赖,根据网络情况可能需要几分钟到十几分钟。看到控制台出现Started Application in xx seconds的日志才算成功。如果启动失败,先看最上面的报错,而不是拉到最后看堆栈尾巴——Spring Boot 的报错信息很长,真正的根因通常在上面几行的Caused by里。

前端启动前先安装依赖:

cd frontend npm install npm run dev

npm install装不上的时候,多半是 Node 版本跟项目依赖的node-sass或node-gyp不兼容。这类报错我会先删掉node_modules和package-lock.json,换个 Node 的 LTS 版本重新装。Vue 2 项目用 Node 14 或 16 最稳,Vue 3 项目 Node 16 以上都没问题。

前端启动后默认跑在localhost:8080或5173。如果发现前端页面能打开但接口请求全部 404,先检查vue.config.js里的代理配置,开发环境前后端分离必须把接口请求代理到后端地址:

const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ devServer: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

我一般把前端开发端口固定成 3000,后端保持 8080。代理配置里changeOrigin: true很关键,它会把请求头里的 Host 改成目标地址,后端做校验时才不会误判来源。开发环境代理配好了,本地就基本不会有跨域问题,跨域的坑主要出在生产环境部署环节。

5.3 用 Nginx 部署到 Linux:打包与静态资源托管

本地跑通只算完成了一半。毕业设计答辩时,如果能把系统部署到一台云服务器上,用 IP 加端口直接访问,答辩效果会好很多。常见做法是前端npm run build打包成静态文件,后端mvn package打成 jar 包,然后 Nginx 托管前端文件并反代后端接口。

后端在服务器上最简单的跑法是直接执行 jar:

nohup java -jar ordering-system.jar --spring.profiles.active=prod > /var/log/ordering-system.log 2>&1 &

nohup和&是为了让 jar 在 SSH 断开后继续运行。日志重定向到文件里,出问题时直接查看,不用纠结控制台输出被终端关闭的问题。前端打包后的dist目录上传到服务器,Nginx 配置里把/api路径反向代理到后端的 8080 端口:

server { listen 80; server_name your_server_ip; root /var/www/ordering-system/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

try_files这段如果漏了,刷新前端页面时会出现 404,这是 Vue Router 的 history 模式导致的。请求/order/detail时 Nginx 找不到对应文件,必须把它引导到index.html,让前端路由自己解析。这个配置基本是 Vue 项目部署的标配,哪个部署文档里没有它,哪个部署文档就是在挖坑。

云端服务器记得在安全组里放行 80 端口和 8080 端口,有些学生部署完发现访问不了,排查半天发现是云厂商防火墙没开,这个细节非常容易让人抓狂。

6. 避坑与排查:从开发到上线的现场记录

6.1 登录成功了,但后续接口全部报 401 或跨域错误

现象:前端登录接口正常返回 Token,本地开发环境一切正常,部署到服务器后所有需要登录的接口全部失败。浏览器控制台要么报 CORS,要么报 401,后端日志里看不到任何请求记录。

原因:生产环境下前端页面在 80 端口,后端接口在 8080 端口,浏览器发起了跨域请求。后端如果没配置 CORS 放行,或者拦截器对OPTIONS预检请求处理不对,就会把真实请求拦在外面。另一个隐蔽原因是前端 axios 的baseURL还是/api,但 Nginx 没有正确转发,请求根本没到后端。

解决:生产环境用 Nginx 反向代理解决跨域,后端同时开启 CORS 配置兜底。AuthInterceptor里务必放行OPTIONS请求;WebMvcConfig里允许指定前端来源、允许Authorization请求头、允许GET/POST/PUT/DELETE方法。两层都配好,跨域问题才能根治,只改一边很容易出现“时好时坏”的玄学状态。

6.2 数据库脚本导入成功,但页面显示中文全是问号

现象:SQL 脚本导入没有报错,后端也能正常查询,但前端页面上的菜品名称、分类名称全部显示成???或者乱码。

原因:数据库连接串里没加characterEncoding=utf8,或者表本身是latin1字符集。MySQL 连接驱动默认的字符集跟数据库不一致,写入和读取的编码就对不上。导入脚本时如果用 Navicat 这类图形工具,文件的编码格式也会影响导入结果。

解决:先把表改成utf8mb4字符集;后端连接串补上useUnicode=true&characterEncoding=utf8;用命令行导入 SQL 脚本前,确认脚本文件本身是 UTF-8 编码。这三步做完,中文显示基本恢复正常。乱码类问题最好一次定位,别一个个表去改,直接在数据库层面批量处理。

6.3 JWT 密钥太短或包含非法字符,后端启动报错

现象:后端启动时报WeakKeyException: Key must be at least 256 bits或者数字签名异常,明明代码看起来没有问题。

原因:JWT 工具类里用Keys.hmacShaKeyFor(secret.getBytes())生成密钥,HS256 算法要求密钥至少 32 个字节。如果配置里只写了my-secret-key这种十几位的字符串,运行时就会直接报错。这类错误不是代码逻辑问题,而是密钥长度跟算法要求不匹配。

解决:把jwt.secret换成至少 32 个字符的随机字符串,或者用工具类生成 256 位密钥再硬编码进配置。还有一点要注意,密钥不要写在代码里,放到application.yml的配置项里,答辩时这个细节也可以作为安全设计的亮点提一句。

6.4 MySQL 8.x 驱动与旧配置不兼容,启动报 SSL 连接错误

现象:后端启动时报Communications link failure,控制台提示The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。

原因:新版 MySQL 驱动默认要求设置服务器时区,而旧项目连接串里没有serverTimezone参数。驱动去读系统时区时拿到了一串乱码,直接判定时区不可识别,连接因此失败。

解决:在连接串后面追加serverTimezone=Asia/Shanghai。如果是 MySQL 5.x,驱动和连接串按老配置来;如果升级到了 MySQL 8.x,驱动用com.mysql.cj.jdbc.Driver。这类问题在日常接手旧项目时高频出现,每次我都会先问一句“你这个 MySQL 是哪个版本”,版本对不上,后面全是白忙。

6.5 菜品图片上传成功,但页面刷新后图片 404

现象:后台管理里上传菜品图片,预览正常,页面一刷新图片就裂了,控制台显示 404。图片明明在项目目录下能看到文件。

原因:开发环境把图片存到了后端项目的target/classes/static目录,这个目录每次重启会被重新编译清空。生产环境如果把图片存到 jar 包内部,更是重启一次丢一次,而且磁盘上的文件路径是临时的。这是典型的“本地能用,上线全废”的路径问题。

解决:图片统一存到服务器固定目录,比如/data/upload/,后端写一个静态资源映射,把/upload/**访问映射到那个目录。如果用云存储,就直接对接阿里云 OSS 或腾讯云 COS,文件持久性比本地磁盘靠谱得多。接手此类项目时,我做的第一件事就是把“文件保存路径”从代码里抽出来放到配置文件,这是用血泪换来的习惯。

7. 答辩与改造:三个让项目不落俗套的进阶方向

拿到一套完整源码,跑通只是起点。真正让毕业设计跟别人拉开差距的,是能讲清楚“我改了什么、为什么改、效果怎么样”。我最推荐加的第一个小功能是“今日销量榜”。在订单明细表里按菜品聚合,统计当天销量,接口返回 Top 10 名单,前端在菜品列表页加一个排行展示。SQL 里用DATE(create_time) = CURDATE()做日期过滤,聚合查询对毕设来说足够简单,但完整覆盖了“统计聚合”这个答辩高频考点。

第二个值得做的是给下单接口加一个简单的并发保护。SELECT ... FOR UPDATE锁住菜品记录,再扣库存,能防止两个用户同时下单导致超卖。这个点不需要复杂的分布式锁,但能把“为什么下单方法要加事务和锁”讲得有理有据。答辩时老师问“多人同时点同一道菜会怎样”,能答出这里的关键就拿到了关键分。

第三个建议是把 JWT 的过期时间单独抽成配置项,写清楚默认 7 天。前端在 Token 过期前提前刷新,或者后端提供续期接口,都能成为系统设计说明里的一个亮点。每次完成一个毕业设计项目,我都会问自己一句:“如果老师让我现场加一个状态,我能不能在十分钟内改完?”答案如果是肯定的,说明这个工程底子真的扎实了。希望这些方案能帮到你,让你少走弯路、少熬夜。

本文还有配套的精品资源,点击获取

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

接近开关选型接线与故障排除实战指南

1. 接近开关到底是个什么东西干自动化这行十几年&#xff0c;接近开关是我见过最“不起眼但离了它真不行”的元件之一。它不像PLC那样引人注目&#xff0c;也不像伺服电机那样动辄上热搜&#xff0c;但产线上十台设备里有八台都藏着它——限位、计数、测速、定位、安全门检测&a…

作者头像 李华
网站建设 2026/9/26 15:44:10

【转】AdoQuery 报 E_FAIL?从 CursorLocation 到 TaoToken 配置的排查清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 15:41:54

TensorFlow2.0汉字手写识别:3755类的完整实现与避坑指南

简介&#xff1a;面向深度学习实践的中文手写汉字识别项目&#xff0c;基于TensorFlow2.0实现&#xff0c;提供一套完整的毕业设计源码。项目覆盖数据集获取与转换、CNN模型构建、训练评估、单字识别预测等环节&#xff0c;适合计算机专业学生用于课程设计、毕业设计或TensorFl…

作者头像 李华
网站建设 2026/9/26 15:40:57

AI图像生成产品化:从Stable Diffusion Demo到企业级API服务

1. 项目概述&#xff1a;为什么要把 AI 图像生成“做成产品能力”而不是“跑个 demo”最近三个月&#xff0c;我陆续帮五家不同行业的客户落地了图像生成类功能——有做电商详情页自动配图的&#xff0c;有给教育平台生成教学插图的&#xff0c;有为本地文旅局批量产出景区宣传…

作者头像 李华
网站建设 2026/9/26 15:40:43

Agent 离线评测实战:用 TaoToken 搭建可复现基准测试集

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华