前几天一个学弟在微信上问我:2026年毕设还能不能用SSM+Vue做美食网站,会不会被导师说技术太老。我直接跟他说,只要你的系统不是只做了个登录注册加两张表,这个组合在答辩现场反而很稳。SSM负责后端业务逻辑,Vue负责前端交互,两者通过JSON通信,是一个经典的前后端分离闭环,评委想挑毛病都不太好挑。这篇文章我打算把整套方案拆开讲清楚,从选题、数据库、后端、前端、部署到论文,顺着一个真实项目的推进顺序来,适合准备做SSM+Vue美食网站方向的应届生,也适合想快速搭一个完整管理系统练手的初级开发。全文尽量不写废话,所有内容都可以直接拿过去改造。
1. 从“会不会太老”说起:SSM+Vue美食方案在2026年为什么依然能打
1.1 “够不够新”不是评价标准,“闭环完整”才是
很多学生选技术栈的时候,第一反应是追新。Spring Boot 3、微服务、Redis、RabbitMQ,名字一个比一个响。但毕业设计本质上是给你完整走一遍“需求分析、系统设计、编码、测试、论文、答辩”的流程,导师更看重过程完整,而不是你塞了多少高级组件。SSM作为经典Java后端组合,搭配Vue做前端,最大的优势是资料多、思路清楚、遇到坑能找到现成解决案例,而且它能完全撑起一个美食网站的CRUD、搜索分页、购物车、订单、评论、视频展示这些核心模块。
另一个现实因素是2026届的答辩老师基本都是从这个时代过来的老师,他们看到SSM不会觉得陌生,看到Vue也不会觉得太新。只要你把代码结构写清楚,功能跑通,论文逻辑合理,分数不会差。反过来,如果选了一个特别极端的新框架,出了问题自己都调不明白,反而容易在答辩台上被追问到沉默。
1.2 美食网站的“用户+商家+管理员”角色矩阵
做美食网站,不能只做一个用户看菜谱的静态页面。毕设要想工作量饱满,我建议直接拆成三个角色:普通用户、商家(或店主)、系统管理员。
- 普通用户:注册登录、浏览菜品分类、搜索菜品、查看菜品详情、加入购物车、下单、评论、收藏、查看个人订单。
- 商家:管理自己的菜品信息、上下架菜品、处理订单状态、回复评论。
- 管理员:管理用户、管理商家、审核菜品分类、统一处理订单、查看统计报表。
这其实已经不是一个“网站”了,而是一个轻量级的电商系统。美食网站只是它的业务外壳,里面该有的核心管理逻辑全都在。评委一看你的用例图、模块图,工作量就能打及格分。而且这三个角色共用一套前后端代码,只是通过角色权限控制不同菜单和操作,技术实现成本并不高,但展示效果很充实。
1.3 功能清单要具体到评审一眼看出工作量
我见过不少同学写需求分析时,只写“用户管理”“菜品管理”“订单管理”这种大词,导师看了等于没看。正确做法是每个模块下都写出可操作的功能点,比如:
- 用户登录后能修改头像,头像图片会回显到导航栏;
- 菜品列表支持按分类筛选、按价格排序、按名称模糊搜索;
- 菜品详情页的图片相册可以左右切换;
- 购物车支持批量勾选、增减数量、实时合计;
- 订单列表支持状态过滤,比如待付款、已发货、已完成;
- 商家端能对订单进行发货操作,发货后用户端状态同步更新。
这些功能点最终可以直接转化为论文里的功能需求表,也能让代码结构更有层次。我不会建议你去凑功能,而是建议你把每个功能想清楚“用户是怎么操作的、数据是怎么流转的、界面是怎么反馈的”,这一个闭环想通,后面写代码会顺畅得多。
2. 功能边界与数据库设计:先画出表格,再谈代码
2.1 核心表结构:从用户到订单的字段设计
数据库是整套系统的地基。我在做毕设的时候吃过亏,因为前期表设计太随意,后面写MyBatis关联查询时反复改SQL。美食网站的核心表大概有这些:用户表、商家表、菜品分类表、菜品表、菜品图片表、购物车表、订单表、订单明细表、评论表、收藏表。
以菜品表为例,比较合理的字段是这样:
- id(主键自增)
- category_id(分类外键)
- name(菜品名称)
- description(菜品描述)
- price(价格,用decimal(10,2))
- stock(库存)
- main_image(封面图URL)
- status(状态:1上架、0下架)
- create_time、update_time
这里有一个很关键的习惯:所有金额字段不要用float,必须用decimal。因为浮点类型在计算购物车合计时会出现精度丢失,最后算出来的价格差几毛钱,非常难看。订单表里要单独存一个order_no订单编号,不要用自增id直接展示给用户,因为商家和用户沟通时需要一个可读的订单号。
评论表的话,建议关联用户id、菜品id、订单id,并加一个reply字段给商家回复。收藏表用user_id加dish_id做联合唯一索引,防止同一用户重复收藏。索引设计虽然不复杂,但能让答辩时被问到“数据库优化”时你有话可说。
2.2 MyBatis的resultMap关联查询怎么设计才不乱
MyBatis是SSM的持久层框架,核心就是写Mapper接口和XML。在做菜品详情页时,一个菜品往往要关联分类名称、图片列表、评论列表。我建议不要用一条巨大的多表JOIN一次性把所有数据查出来,而是分层次查询,比如:
- 先查菜品基础信息和分类名称;
- 再查菜品图片列表;
- 最后分页查评论列表。
这样每个Mapper方法职责单一,XML好维护。如果你非要一次查出嵌套对象,可以使用collection标签来映射List。举个例子,在菜品详情返回结果里,我需要菜品分类名和图片集合,那么在resultMap里可以这样配置:
<resultMap id="DishDetailMap" type="com.example.entity.Dish"> <id property="id" column="dish_id"/> <result property="name" column="dish_name"/> <result property="categoryName" column="category_name"/> <collection property="images" ofType="com.example.entity.DishImage"> <result property="imageUrl" column="image_url"/> </collection> </resultMap>这样返回的Dish对象里就会自动带上images列表,前端拿到之后直接循环渲染就行。不过要提醒你,collection嵌套查询不能在一对多场景下配合PageHelper做分页,否则分页的count会算错。如果你遇到列表分页与详情嵌套同时存在的场景,我一般会拆成两个查询,详情查详情、列表查列表,各管各的,性能更好,思路也清晰。
2.3 登录权限的数据基础:简单角色判断还是RBAC
很多毕设系统做权限,直接在每个表里加一个role字段,值为user、merchant、admin,然后后端拦截器判断角色。这种方案对美食网站完全够用,而且代码量小,适合写在论文里讲清楚。如果采用RBAC(用户-角色-权限),会引入角色表、权限表、用户角色关联表、角色权限关联表,虽然看起来更专业,但对毕设而言会增加不少冗余代码。
我建议选择前者:user表里加role字段,菜单通过前端路由动态显示。后台只需要写一个拦截器,判断用户是否已登录;需要更细粒度控制的接口,在Controller方法上加个注解或者用拦截器路径匹配来限制。这样做的好处是代码容易读,论文里也容易解释。千万不要把权限设计成“前端隐藏按钮”就算完了,后端接口必须校验,否则随便一个人调接口就能删掉菜品,答辩时被安全问题一问就露馅。
3. 后端SSM的落地姿势:Controller、Service、Mapper三层到底怎么配合
3.1 统一返回结构与全局异常处理
后端接口如果每个人自己返回Map、或者直接返回Result,前后端联调会非常痛苦。我建议在项目开始前就定义好一个统一返回类,比如Result,包含code、message、data三个字段。成功时code为200,失败时code为500或者业务码。前端所有请求都拿这个结构做判断,弹提示时直接用message字段。
下面是一个常见的Result结构:
public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }然后写一个全局异常处理器,用@ControllerAdvice捕获业务异常、参数校验异常,统一返回Result.error。这样即使代码运行抛错,前端拿到的还是正常JSON结构,而不是一堆堆栈信息。这个细节放在论文“系统实现”里很加分,因为说明你考虑到了系统的健壮性。
3.2 登录态与Token:从Session到JWT的取舍
SSM传统做法是使用Session保存登录状态,配合Cookie自动携带。但如果前端是Vue,并且以后可能要扩展小程序,我建议直接用JWT。JWT的本质是服务端生成一个包含用户信息的签名Token,前端每次请求时在请求头里加上Authorization字段。后端拦截器解析Token,从中获取用户id和角色。
生成Token的核心代码大致是这样:
String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();Vue前端通过Axios请求拦截器把Token加到headers里,响应拦截器判断如果code是401,就跳到登录页。这里有一个坑:JWT的密钥不能写死在代码里提交到Git,最好放配置文件。另外,登出功能只需要前端删除本地Token,并请求一下后端让用户状态失效。对毕设来说不用做太复杂的刷新Token机制,但要在论文里说明清楚过期时间设置。
3.3 搜索、分页、排序:PageHelper与动态SQL的配合
SSM里做分页,大部分人用PageHelper,它依赖MyBatis拦截器,使用非常简单。在执行查询前调用PageHelper.startPage(pageNum, pageSize),后面的查询就会自动带上LIMIT,并生成PageInfo对象,里面包含total、pages、list这些字段。前端ElementUI的el-table配合分页组件时,只需要请求pageNum和pageSize两个参数即可。
搜索功能的实现要结合动态SQL。假如菜品列表页有名称、分类、价格区间三个筛选条件,那么Mapper XML里可以这样写:
<select id="findDishList" resultType="com.example.entity.Dish"> SELECT * FROM dish <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> </where> ORDER BY create_time DESC </select>用where标签的好处是自动剔除多余的AND,避免SQL语法错误。排序的话,为了避免前端传任意字段导致SQL注入,我会在后端做一个白名单,比如只允许按price、sales、create_time三个字段排序,前端传别的字段就忽略。
3.4 文件上传和视频播放:图片、m3u8这类需求的处理方式
美食网站肯定有菜品图片,如果有菜谱演示视频就更完整。图片上传我推荐存本地磁盘或者云OSS。如果是本地存,需要在配置文件中指定一个上传目录,然后通过一个虚拟路径映射来访问,例如把D:/upload映射到/upload/**。上传接口接收MultipartFile,生成UUID文件名,保存后返回URL。
热搜词里有一个“vue播放m3u8”,这是因为视频处理一般不会直接上传MP4大文件,而是用ffmpeg转成m3u8切片,以便浏览器使用HLS协议播放。毕设阶段我不建议自己写转码逻辑,你可以调研一下:后端可以直接用ffmpeg命令把上传的视频转成m3u8格式,然后前端用video.js或者hls.js播放。如果你打算轻量处理,就直接上传MP4,浏览器原生video标签也能播放,只是论文里的技术含量会低一点。如果想让论文有亮点,可以在系统实现章节写“采用HLS协议实现菜谱视频点播”,但前提是你真的跑通了m3u8播放。
4. Vue前端的工程化细节:路由、状态、列表与视频播放
4.1 项目初始化与目录结构:Vue2还是Vue3,这是个选择
SSM后端对应的前端,我见到的很多是Vue2 + ElementUI,也有用Vue3 + ElementPlus的。从稳定性和网上资料数量来看,Vue2的技术方案更成熟,遇到问题基本能查到答案;但Vue3的Composition API在写复杂组件时确实更舒服。我的建议是,如果你的导师不强制,选Vue2 + ElementUI会更稳;如果自己对Vue3已经比较熟,就选Vue3。
不管选哪个,目录结构一定要清晰。我会把前端拆成api、router、store、views、components、utils这几个目录。api目录专门放请求函数,比如dish.js、order.js;router放路由配置;store放登录状态和用户信息;views放页面组件。这样做的好处是,论文里写“系统采用模块化设计”时,结构图直接从这个目录结构画出来。
4.2 路由配置与登录守卫:Vue Router的参数传递
美食网站大概率会有这样几个页面:首页、菜品列表、菜品详情、购物车、订单、个人中心、后台管理。Vue Router配置时,菜品详情页需要接收菜品id,一般通过路由参数传递。比如路由定义是:
{ path: '/dish/detail/:id', name: 'DishDetail', component: () => import('@/views/dish/DishDetail.vue'), meta: { requiresAuth: true } }在详情页里通过route.params.id获取菜品id。如果你用query传递,就得用route.query.id。两者区别在于,params是路径参数,比较规整;query是?后面的参数,适合筛选条件。我在做列表页跳详情时习惯用params,因为用户分享链接时URL更友好。
路由守卫用来控制访问权限。登录状态存放在Vuex或Pinia里,在全局前置守卫中判断:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next({ path: '/login' }); } else { next(); } });如果还牵扯到管理员和商家的权限,可以在meta里加roles数组,在守卫里同时判断角色。
4.3 列表页的常用组件写法:搜索、分页、全选与el-select远程搜索
美食网站首页、菜品列表、订单列表,大量用到表格和搜索条件。ElementUI里el-table支持多选,只要加一列type="selection"的列,然后通过@selection-change事件拿到选中数据。
这里经常遇到的坑是:全选按钮勾选后,切页或者修改搜索条件时,之前选中的项会丢失。这是因为el-table的selection列默认绑定的是当前页数据。如果购物车是全选状态,我又想跨页批量删除,就需要在拿到selection-change事件时,自己维护一个selectedIds数组,而不是依赖组件内部状态。
handleSelectionChange(rows) { this.selectedIds = rows.map(row => row.id); }跨页时要先把上一页的选中项合并到map里,再根据当前页变化重新渲染。这个细节我放到第7章细说,因为绝对是实操高频问题。
el-select的远程搜索也是热搜词里的高频点。在美食网站里,给菜品关联食材,或者管理员在后台从用户列表里搜索用户时,都可以用远程搜索。ElementUI的el-select支持filterable和remote属性,配合remote-method函数:
<el-select v-model="selectedUser" filterable remote reserve-keyword placeholder="请输入用户名搜索" :remote-method="searchUser" :loading="loading"> <el-option v-for="item in userOptions" :key="item.id" :label="item.username" :value="item.id"> </el-option> </el-select>remote-method里调用后端接口,把返回结果赋值给userOptions。需要注意,远程搜索会频繁触发接口,一定要加一个简单的防抖,比如用setTimeout做300毫秒延迟,否则每次输入都会发请求,接口压力很大。
4.4 计算属性computed、侦听器watch和Vue的diff算法
在美食网站里,购物车合计金额非常适合用computed计算。你不需要每次点加号都手动调用一个方法去修改合计,而是定义computed,依赖购物车列表自动计算:
computed: { totalPrice() { return this.cartList.reduce((sum, item) => sum + item.price * item.count, 0); } }这样只要cartList中任何一项价格或数量变化,totalPrice自动更新。computed有缓存,性能比methods好。watch则适合监听某个数据变化后去触发异步操作,比如监听搜索关键字变化后重新请求列表,但也要配合防抖。
热搜词里有一个“vue watch数组的第一项为啥新值和旧值是一样的”,这是一个经典问题。原因是数组内部元素变化时,Vue的watch默认是浅监听,新旧值指向同一个数组引用。解决办法是开启deep: true,或者用computed返回一个新数组再监听。比如:
watch: { cartList: { handler(newVal, oldVal) { console.log('购物车变化'); }, deep: true } }diff算法则是Vue底层渲染优化的核心,面试和答辩时经常被追问。Vue渲染页面时会生成虚拟DOM,更新时通过diff算法比较新旧虚拟DOM的差异,最后只更新变了的那部分。你不需要背源码细节,但要说清楚“为什么列表渲染要加key”。key的作用是让Vue能够准确复用和移动DOM节点。如果不加key,列表更新时可能出现状态错乱。这个点放在项目总结或技术难点里非常加分。
5. 前后端联调与上线的坑:从跨域到部署的完整闭环
5.1 跨域与开发环境代理:前端配一次就够
SSM后端跑在8080端口,Vue开发服务器跑在8081或5173端口,两者之间直接发请求会触发跨域。解决跨域的办法很多,CORS配置、前端代理、Nginx反向代理都是常见方案。我推荐在开发阶段用Vue的proxy代理,这样前端的请求地址写成/api,然后由Vue开发服务器转发到后端。
Vue CLI的vue.config.js是这样配置的:
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };这样你在前端调接口时写/api/dish/list,实际会转发到后端/dish/list。开发时就不需要再关心CORS。如果后端没有加CORS过滤器,生产环境部署时也可以用Nginx配置反向代理统一解决。
5.2 生产部署:后端Tomcat、前端Nginx,注意静态资源路径
后端SSM项目一般打war包放到Tomcat的webapps目录下,或者如果用了Spring Boot打成jar包直接java -jar运行。传统SSM项目我建议直接打war包,论文里可以写“部署在Tomcat服务器”。前端Vue项目执行npm run build后,会生成dist目录,把dist目录里的文件放到Nginx的html目录下。
我这里要给一个重点提醒:前端Vue Router如果用history模式,部署到Nginx后刷新页面会404。解决办法是在Nginx配置里加一个location /,尝试解析所有路径,找不到文件就返回index.html:
location / { try_files $uri $uri/ /index.html; }否则你从列表页点进详情页没问题,但直接刷新详情页URL就会报404,这一点很多第一次部署的同学都会踩。
5.3 启动卡住、后台进程和日志排查思路
热搜词里那条“98% after emitting CopyPlugin vue启动时卡住不动”非常有代表性。我自己也遇到过,看到这个提示基本不是代码逻辑错误,而是构建阶段卡在拷贝静态资源上。常见原因是系统内存不足、webpack配置中有循环依赖、或者杀毒软件拦截了文件读写。
解决思路是先分清楚是dev服务器卡住还是build卡住。如果是npm run serve卡在98%,我一般会先关掉所有浏览器缓存插件,再检查node_modules版本是否冲突,最后尝试增加Node内存限制:
export NODE_OPTIONS=--max_old_space_size=4096如果是build时卡住,可以检查是否引入了过大的静态资源文件,比如一个几十MB的视频被放到了src目录里,webpack会尝试把它打包处理。正确的做法是把大文件放到public目录或用URL路径引用,不要走import引入。
6. 毕设论文和答辩的组织逻辑:让代码与论文互相成就
6.1 论文大纲:从绪论到总结的完整路线
写论文最忌讳的是最后几天开始拼凑。我建议在代码开发之前就搭好论文大纲,然后一边写代码一边填充内容。美食网站论文的标准结构大概是:
- 绪论(研究背景和意义、国内外研究现状、主要工作)
- 相关技术介绍(SSM框架、Vue、MySQL、Tomcat、Maven)
- 系统分析(可行性分析、需求分析、用例图、功能需求)
- 系统设计(总体架构、功能模块设计、数据库设计)
- 系统实现(每个核心模块的实现思路、关键代码、运行截图)
- 系统测试(测试环境、测试用例、测试结果)
- 总结与展望
每一章都不需要写太长,但必须言之有物。特别是相关技术介绍,不要直接抄百度百科,最好结合自己的项目来写。比如写SSM时,可以说明“在系统中Spring负责Bean管理,SpringMVC负责请求映射,MyBatis负责数据持久化”,这样导师会觉得你是真的理解。
6.2 需求分析和数据库设计怎么写才不空洞
需求分析这章,要画用例图。如果你是用户、商家、管理员三个角色,用例图会很丰富。每个角色有多个用例,用例之间还可以有包含和扩展关系。如果不会画复杂的UML,用在线绘图工具画简洁用例图就可以。数据库设计要附上ER图和关键表字段表,不要贴一大堆SQL代码。把每张表的字段名、类型、含义列清楚,重点说说哪些字段设置了索引,为什么设置。
我记得写过用户表和菜品评论表的关联时,用了外键逻辑但没有真正在数据库里加外键约束,这是有意为之。因为互联网项目为了性能和扩展性,一般用逻辑外键而不是物理外键。这个点在论文里可以单独提一句,显得你有工程意识。
6.3 关键代码和测试报告的“有效”写法
论文里的核心代码不是越多越好,而是要根据功能模块选一段能说明关键思路的代码。比如JWT拦截器、PageHelper分页、Vue路由守卫,这几段代码一定要放,并且用一两句话解释核心逻辑。运行截图也不要全篇都是截图,核心模块截图配上说明文字。
测试部分不能只写“测试通过”。我建议列一个测试用例表格,包括测试模块、测试步骤、输入数据、预期结果、实际结果、是否通过。比如“用户登录测试:输入正确用户名密码,预期跳转首页,实际跳转首页,通过”。这样的表格实实在在,导师看了不会觉得凑数。对于Bug修复过程,也可以挑一个代表性Bug,比如图片上传后无法显示,写出问题现象、排查过程、最终解决方案,这比复制十页代码有价值得多。
6.4 答辩高频问答:Vue路由、computed、diff算法别含糊
答辩时老师最喜欢问“你项目里难点是什么”和“某个技术原理是什么”。如果你在项目总结里写了自己用Vue做了很多交互,就要准备好Vue相关问题。常见高频问题包括:
- Vue Router的两种模式hash和history有什么区别?
- computed和watch的使用场景分别是什么?
- v-for为什么要加key?
- diff算法大概是怎么工作的?
- 跨域是怎么解决的?
- 数据库分页查询是怎么实现的?
- Token会不会被伪造?
这些问题不需要回答得特别深,但一定要能说清楚。比如hash模式是URL带#号,不刷新页面也能切换路由,history模式是HTML5 History API实现,刷新时会请求服务器,所以部署要配置Nginx try_files。这些点要能脱口而出。
7. 那些热搜里的高频问题,其实就是你的答辩加分题
7.1 “98% after emitting CopyPlugin”这类构建卡死问题
前面提过这个热搜词,它确实是我在毕业设计期间真实遇到的问题。当时做美食网站前端,页面还没写完,npm run serve启动总是卡在98%,浏览器半天打不开。后来发现是我把一堆菜品图片直接放在src/assets下面,webpack在build阶段会拷贝资源,图片太多太大导致内存占用高。解决方案是图片全部改放到public/img目录,通过绝对路径引用,构建速度立刻恢复正常。
如果你也遇到类似问题,可以按这个顺序排查:先看是否有超大文件在src目录;再检查vue.config.js里是否配置了复杂的loader;最后看系统的Node版本和内存。这个经验写进论文的前端性能优化小节,比单纯写“使用懒加载”更有说服力。
7.2 beforeCreate钩子栈溢出:不是复杂问题,但能考住人
热搜词里还有一条“[vue warn]: error in beforecreate hook: rangeerror: maximum call stack size”,这是Vue生命周期钩子里最经典的错误之一。大多数情况下,在beforeCreate钩子中去访问data数据或调用methods方法会出问题,因为此时数据还没初始化。另一个常见原因是data里某个属性初始化时引用了自身,形成死循环。
比如有同学为了初始化一个空对象,写了这样的代码:
data() { return { info: this.info } }这肯定爆栈。正确做法是直接把info声明为null或者一个字面量对象。在答辩时如果老师问Vue生命周期,你可以把created和mounted的区别、beforeCreate和created的区别都讲清楚,这个错误经历反而能让回答更真实。
7.3 全选按钮状态不同步与el-select远程搜索的细节
这里是实操角度的补充分享。ElementUI的el-table全选按钮,如果通过手动改变表格数据源来重置选中状态,有时会出现“选中状态残留”。我的解决思路是给el-table加一个ref,在改变查询条件后调用clearSelection()方法。同时,我维护了一个Map来记录跨页选中的id,避免分页后丢失选中项。
el-select远程搜索还有一个细节:当远程搜索返回结果后,如果v-model绑定的值在options里找不到对应的option,那么输入框会显示原始值而不是label。解决办法是在赋值给el-option时,额外从后端返回的列表里把对应的label保存下来,或者使用el-select的value-key。这个点虽然小,但特别容易让人抓狂,尤其是你在做“修改菜品时选择分类”的功能,打开回显却是空白的,就是这个原因。
7.4 文件上传乱码、Excel导入导出和路径问题
美食网站后台可能有导出订单Excel的需求。Java里用Apache POI导出Excel,最容易出乱码的是文件名。解决办法是在响应头里设置Content-Disposition并做URL编码:
String fileName = URLEncoder.encode("订单列表.xlsx", "UTF-8"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);文件上传路径问题也值得注意。很多同学把上传路径写死为Windows的D:/upload,交给老师后换机器跑就找不到文件了。我建议在配置文件里用相对路径或者系统属性动态拼接,比如:
file.upload-path=${user.dir}/upload这样项目放在哪个目录,上传文件就在项目目录下的upload文件夹里,换机器也能正常跑。
最后再聊一句
做完这套SSM+Vue美食网站,我的感受是:毕设最难的从来不是技术,而是你有没有把从零到一的过程完整走一遍。只要你认真搭了数据库、写了十几张表的CRUD、调过前端接口、部署过Nginx,答辩时心里就很踏实。这里再分享一个小技巧:把所有你踩过的坑,按照“问题描述、排查过程、解决办法”的记录格式整理成一份笔记,放到论文的测试或工作总结里。这种做法不但让论文更饱满,也能让你在答辩时面对“你遇到过什么困难”这种问题时,脱口而出真实案例,而不是临时编。祝你的2026届毕设顺利,代码一遍过,论文一次改。