
如果你正在准备Java方向的毕业设计并且想选一个“难度适中、容易出成果、答辩好讲”的题目那“网上点餐系统”绝对是个经典选项。它在技术栈上是标准的SpringBoot Vue MySQL业务上又完整覆盖了点餐、购物车、订单、权限管理这些电商核心场景既能体现全栈能力又不会像秒杀系统那样把自己逼到性能调优的深坑里。这一篇就结合我实际带过项目的经验把这个“网上点餐系统”从里到外拆开讲一遍。不止是摆出一份源码结构就算了我会把数据库表为什么这么设计、订单状态为什么要用状态机、管理员登录和用户登录怎么区分、前端购物车怎么和后端同步这些关键决策都讲清楚同时把部署和写论文、准备答辩的套路也一并盘出来。无论你是打算直接改造这份源码还是想照这个思路从零手写一个这篇文章都能帮你少走不少弯路。1. 项目整体设计与技术方案选型1.1 为什么这个技术组合是毕业设计的最优解先说结论SpringBoot Vue MySQL 这个组合在毕业设计这个场景下几乎没有短板。SpringBoot负责后端它的核心优势是“约定优于配置”。以前用SSMSpring SpringMVC MyBatis搭一个项目光是XML配置文件就能写半天而且每换一个环境就要改一堆东西。到了SpringBoot这里内置Tomcat、自动装配、Starter机制一个main方法就能把Web服务跑起来这对时间紧张的毕设党来说太关键了。而且它天然支持RESTful API、JWT认证、拦截器、全局异常处理这些常用功能代码写起来很规整论文里的“系统架构图”也能画得明明白白。Vue负责前端它是个渐进式框架入门曲线比React要平缓得多。对于不常写前端的人来说组件化开发、指令绑定、响应式数据这套思路两三天就能上手。配合Element UI这类组件库后台管理的页面基本不用自己写样式拖拖拽拽拼拼凑凑就是一个还不错的界面。更关键的是Vue的功能边界很清晰——页面渲染、用户交互、调后端接口它管好这三件事就够了不需要你花大量时间研究什么虚拟DOM优化、打包性能调优。MySQL作为数据库就更不用说了免费、轻量、资料极多。点餐系统的数据模型不复杂无非是用户、菜品、分类、订单、订单明细这几张核心表用MySQL完全撑得住。配合Navicat或者MySQL Workbench做可视化操作建表、导数据、写存储过程都非常方便。有人会问那用Redis做缓存、用RabbitMQ做消息队列不是更高级吗确实更高级但毕业设计的评分核心是“完整度 逻辑合理性 论文规范性”不是“技术新奇度”。你引入Redis就要解释缓存穿透、缓存雪崩怎么解决引入MQ就要讲消息可靠投递这些复杂度会在论文里变成你躲不开的难题。我的建议是如果基础一般先把这套经典三件套吃透做到每个接口、每张表都能讲清楚原理这比堆一堆中间件但一问三不知要强太多。基础好、时间充足的话后面我讲到订单模块时会提一下可以在哪些位置优雅地引入Redis和MQ作为论文里的“系统优化”章节素材。1.2 网上点餐系统的业务流程梳理搞清楚系统该有哪些功能之前先要把业务流程理清楚。网上点餐系统的核心流程其实不复杂一句话就能概括用户浏览菜单把菜加入购物车生成订单完成支付商家或者管理员处理订单。但这个流程拆开看就涉及三个不同的使用角色角色核心操作关注点普通用户浏览菜品、加入购物车、下单、支付、查看订单、管理收货地址操作顺畅、菜品信息清楚商家/管理员菜品上下架、修改库存、处理订单接单/出餐/完成、查看营业统计订单管理高效、菜品维护方便系统管理员用户管理、分类管理、基础数据维护系统整体可控这里有个关键决策用户端和管理端是做成一个系统还是拆成两个从实际项目经验看最好拆成两个独立的前端应用后端可以共用一个服务。为什么因为用户端页面要求简洁、移动端适配好而管理端是信息密集的后台表格风格两者混在一个项目里路由配置会非常混乱。拆开之后前端可以建两个Vue工程一个面向用户点餐端一个面向管理者管理端后端用同一个SpringBoot服务通过接口路径前缀比如/api/user/**和/api/admin/**来区分权限。1.3 前后端分离架构下的数据交互这个系统采用前后端完全分离的架构后端提供RESTful API前端通过HTTP请求调用接口。整个交互链路是这样的用户在前端页面操作点餐、加入购物车、提交订单Vue封装好请求参数用Axios发HTTP请求到后端接口SpringBoot接收到请求经过拦截器验证用户身份JWT请求进入Controller层调用Service层处理业务逻辑Service层操作MapperMyBatis或MyBatis-Plus读写MySQL后端返回统一的JSON结构一般包含code、message、data三个字段前端根据返回结果更新页面状态关于返回结构我会强调一下。很多新手喜欢直接返回一个实体对象或者把数据堆在Map里这样其实不好。一定要统一返回结果类比如{ code: 200, message: 操作成功, data: {...} }。这样做的好处是前端处理逻辑非常统一——看到code不等于200就弹错误提示等于200再取出data渲染页面。这在论文的功能测试章节里也很好写直接列几张不同返回码的接口调用截图就够了。2. 数据库设计表结构是系统的地基2.1 核心表的总体结构点餐系统的核心数据是“用户下了什么单、订单里有哪些菜、数量是多少”。围绕这个核心数据库表至少要包含以下几张表名功能说明关键字段user用户表id, username, password, nickname, avatar, role, phone, create_timecategory菜品分类表id, name, sort, create_timedish菜品表id, category_id, name, image, description, price, status, create_timecart购物车表id, user_id, dish_id, number, create_timeorders订单表id, order_no, user_id, total_amount, status, address_id, pay_time, create_timeorder_detail订单明细表id, order_id, dish_id, dish_name, dish_image, price, numberaddress收货地址表id, user_id, name, phone, province, city, detail这个结构是几经打磨后的结果。我见过不少毕设源码把菜品信息直接冗余在订单表里或者一张cart表不记录菜品ID只记录购物车总价这些都是后期会踩坑的设计。订单表orders和订单明细表order_detail一定要分开设计。因为一个订单里可能有多道菜每道菜有自己单独的数量和价格这天然就是一对多的关系。如果把菜品全部拼在一个字段里比如用逗号分隔菜名查询的时候确实方便但要在论文里解释清楚就费劲了而且后续你想做“哪个菜卖得最好”这种统计查询用分隔符存的数据根本没法写SQL。2.2 订单明细为什么必须做菜品快照这是很多人容易忽略的一个点订单明细表里除了dish_id还必须有dish_name、dish_image、price这几个冗余字段。换句话说下单那一刻的菜品信息被“快照”下来了。为什么要这么做因为菜品的价格是会变的。用户下单时一道菜是18元第二天店家把价格改成了20元那用户之前的订单应该按哪个价格算如果你在订单明细里只存了dish_id那查询历史订单时价格就跟着菜品表走了结果就是历史订单金额全对不上。这不是设计缺陷是数据一致性的问题。所以正确的做法是下单时把菜品名称、图片、价格都复制一份到订单明细表里。这样即使菜品后来被删除或改价历史订单依然能完整展示。这个点我在指导毕设时特别会强调因为论文答辩时评委老师经常抓这种细节来问。2.3 容易被忽略的关键字段设计除了一对多关系的处理还有几个字段设计上的细节会直接影响项目的健壮性第一个是金额字段的类型。价格必须用DECIMAL(10, 2)绝不能用FLOAT或者DOUBLE。因为浮点数在计算机里是二进制近似存储的算金额很容易出现0.1 0.2 0.30000000000000004这种情况。用DECIMAL类型MySQL底层是用定点数存储的精度有保证。第二个是状态字段的取值规范。订单状态、菜品上下架状态、用户启用状态这些都应该用TINYINT且固定取值范围并且在后端用枚举或者常量类定义好。我写过一篇文章专门吐槽过“状态魔法值”——代码里到处是if(status 1)、if(status 2)过两个星期自己都记不清1和2分别是什么含义。正确的做法是在Java里定义一个OrderStatus常量类或者枚举比如public enum OrderStatus { UNPAID(0, 待支付), PAID(1, 待接单), ACCEPTED(2, 已接单), DELIVERING(3, 配送中), COMPLETED(4, 已完成), CANCELED(5, 已取消); }第三个是审计字段的统一规划。每张业务表都应该有create_time和update_time而且update_time最好是自动更新。MySQL里的写法是create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间,这样在插入和更新数据时时间字段完全不用Java代码去维护减少人为出错的可能。在MyBatis-Plus里也可以用TableField(fill FieldFill.INSERT)配合MetaObjectHandler来做自动填充效果一样。第四个是逻辑删除。用户删除地址、管理员删除菜品这些操作不要直接物理删除DELETE FROM而是加一个deleted字段删除时执行UPDATE ... SET deleted 1。逻辑删除的好处是数据可回溯万一误删还能恢复而且从论文角度来说写“系统采用逻辑删除保证数据可追溯”也能加一点印象分。3. 后端实现SpringBoot核心模块实战解析3.1 项目目录结构与分层思想后端项目拿到手第一步先看它的包结构。标准的SpringBoot项目包结构应该是这个样子的com.example.restaurant ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl // 业务实现类 ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 数据传输对象 ├── vo // 视图对象 ├── config // 配置类WebMvcConfig、CorsConfig、JwtConfig ├── common // 通用类Result、ResultCode、异常处理 ├── interceptor // 拦截器JWT拦截、Admin拦截 ├── utils // 工具类JwtUtil、FileUtil └── RestaurantApplication.java这个分层结构不是拍脑袋定的而是遵循了“高内聚、低耦合”的设计原则。Controller只管接收参数和返回结果不写任何业务逻辑Service负责具体业务处理Mapper只负责数据库读写。这样做的好处是如果后续要改数据库方言比如从MySQL换成PostgreSQL只需要改Mapper层和依赖配置业务层完全不用动。我看到很多毕设项目的通病是Controller里写了一大堆业务逻辑Service层形同虚设。这样做开发的时候确实爽代码全堆在一个文件里但写论文的时候就麻烦了——你很难用什么“三层架构”“Controller-Service-Mapper”这种分层图来展示你的系统设计。而分层清晰的代码论文里连流程图都能画得顺理成章。3.2 JWT身份认证与权限控制网上点餐系统涉及两种角色普通用户和管理员。用户要能登录下单管理员要能进入后台管理菜品和订单。如果不同角色共用一套接口就会有越权访问的风险。解决方案是用JWTJSON Web Token来管理登录状态。JWT的原理可以简单理解成这样用户登录成功后后端生成一串加密的Token返回给前端前端把Token存到localStorage里每次发请求时在Header里带上Authorization: Bearer token。后端通过JwtUtil解析这个Token就能知道当前登录用户的ID和角色不需要在服务端保存Session。这个机制的实现分几步// 1. 登录成功后生成JWT public String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }生成的时候我会在Token里附带userId和role这两个信息有效时长设为24小时。Token里不存敏感信息只存身份标识这是安全性的基本要求。有了Token之后就要在请求进来时校验。这里用SpringBoot的HandlerInterceptor是最优雅的方案public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从Header中获取Token String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(401, 未登录); } // 解析Token验证有效性 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }但这里有个权限区分的问题。用户端接口和管理端接口的权限是不能一样的。比如一个普通用户调了“删除菜品”的接口那不就出事了嘛。所以在写拦截器时我一般会配置两条规则/api/user/**路径只需要验证登录状态任何角色都可访问而/api/admin/**路径则需要验证角色必须是管理员。对应到WebMvcConfig里的注册方式registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/admin/login);登录接口一定要放行不然还没拿到Token就被拦死了。这里有个小坑如果你用的前端跨域配置是另开的CorsInterceptor那要注意拦截器执行顺序避免跨域请求在OPTIONS预检阶段就被JWT拦截器拒绝。相关处理我在后面排错部分会详细说。3.3 订单状态机的设计细节订单模块是整个系统里业务逻辑最复杂的部分。它涉及的状态不是一成不变的而是会按照业务规则逐步流转。如果直接把状态判断散落在各种if-else里代码可读性极差状态转换的合法性也很难控制。我推荐用状态机的方式来管理订单状态流转。先说这个系统的合法流转路径待支付(0) ── 待接单(1) ── 已接单(2) ── 配送中(3) ── 已完成(4) │ │ │ └── 已取消(5) └── 已取消(5)可以看到不是任何状态都能跳到任何状态的。比如“已完成”的订单不可能再变成“待支付”对不对所以代码里要防止非法流转。实现方式可以很简单写一个订单状态流转工具类或者用枚举的next方法public enum OrderStatus { UNPAID(0) { Override public OrderStatus next() { return PAID; // 待支付只能流转到待接单或取消 } }, PAID(1) { Override public OrderStatus next() { return ACCEPTED; } }; private final Integer value; public abstract OrderStatus next(); }当然这只是最简单的一种写法落地的代码需要在业务Service里加判断。比如用户点击“取消订单”时Service里要判断当前状态是否为待支付或待接单只有这两种状态才允许取消。管理员点击“完成订单”时要判断状态是否为配送中。这些校验逻辑不是可有可无的而是业务严谨性的体现也是论文里可以着重描述的“系统处理非法操作的容错设计”。订单号我建议用时间戳随机数生成比如yyyyMMddHHmmss 6位随机数可以自己在工具类里写也可以用IdWorker或者UUID。但注意订单号要保证唯一性且尽可能具有可读性。3.4 购物车接口设计的取舍购物车是个很有意思的模块。它的业务操作很清晰添加菜品、修改数量、删除条目、清空购物车、查询购物车列表。实现上有纯后端存储存数据库和纯前端存储存localStorage两种方案。结合毕业设计的场景我强烈建议购物车数据存在后端数据库。原因很简单后端存购物车才能真正体现你对接口设计的能力同时也能和前端的登录体系做绑定——用户在A设备上加购的菜换到B设备登录后依然能看到。这在论文的系统设计部分可以写“基于用户维度的购物车方案”加分项就来了。但这里有个细节要注意购物车表的设计需要唯一约束。怎么理解比如用户第一次把鱼香肉丝加入购物车时表里插入一条记录dish_id5, number1第二次再把鱼香肉丝加入购物车时程序应该做的是把原来那条记录的number从1更新为2而不是再插入一条dish_id5, number1的记录。否则购物车列表里出现两行一模一样的鱼香肉丝用户看着就乱套了。实现上一个可行的方案是在Service层先查一下“当前用户是否已经拥有该菜品在购物车”有就update没有就insert。如果更规范一些给user_id和dish_id加上联合唯一索引然后用ON DUPLICATE KEY UPDATE写SQL一次性解决INSERT INTO cart (user_id, dish_id, number) VALUES (#{userId}, #{dishId}, 1) ON DUPLICATE KEY UPDATE number number 1这个写法既避免了多次查询的开销又保证了数据的唯一性。类似的思路在收藏、点赞这种“以用户目标对象”为维度的表设计里都可以复用。4. 前端实现Vue从页面到联调全过程4.1 前端工程的划分与页面架构按照前面的思路前端拆成两个独立工程——用户端customer-web和管理端admin-web。两个工程共享同一套后端接口所以公共的Axios请求封装可以复制一份。用户端的页面不多但每种页面都有自己的职责页面路由路径核心功能首页/轮播图、菜品分类导航、菜品列表菜品详情/dish/:id菜品大图、价格、描述、加入购物车购物车/cart查看购物车、改数量、删除、结算下单确认/checkout选择地址、备注、确认金额、提交订单订单列表/orders查看历史订单、状态筛选订单详情/order/:id订单状态、菜品明细、取消操作登录/注册/login表单校验、Token存储管理端的页面则偏向信息管理和数据展示页面路由路径核心功能仪表盘/dashboard菜品数、订单数、今日营收等统计卡片菜品管理/dish菜品列表、上下架、新增/编辑菜品分类管理/category新增/编辑/删除分类订单管理/orders订单列表、按状态筛选、处理订单用户管理/user用户列表、禁用/启用前端路由设计有个细节要提醒用户端在跳转菜品详情时路由路径上要带上菜品ID比如/dish/12。在Vue Router里获取这个参数的方式是this.$route.params.id如果是Vue Router 4Vue3则是route.params.id。很多新手会在这里卡住因为路由传参分query和params两种用错了拿到的就是undefined。简单区分一下路径后面跟?id1这种是query通过route.query.id拿路径上/dish/:id这种是params通过route.params.id拿。4.2 用户端核心页面的实现思路用户端首页是门面展示的是菜品分类和菜品列表。这里结合Element UI或者Vant组件库做一个清爽的卡片式布局就够了。数据是后端返回的JSON前端通过Axios获取再渲染整体不难重点在于页面和交互的完整。购物车页面稍微复杂一点因为要支持选择菜品、修改数量、实时计算总价。这个场景很适合用Vue的computed计算属性computed: { totalPrice() { return this.cartList.reduce((sum, item) { return sum item.price * item.number; }, 0); }, totalCount() { return this.cartList.reduce((sum, item) { return sum item.number; }, 0); } }计算属性有一个好处是购物车里的菜品数量一旦变化总价和总数量会自动重新计算不需要手动去更新。这个在Vue面试题里也经常被问到要是能在答辩时主动提一句“这里用computed而不是methods来实时计算总价是因为computed有缓存且只在依赖变化时重新计算”绝对是加分项。下单确认页的核心是地址选择和金额确认。用户从地址列表里选一个收货地址然后点击提交订单。提交订单后后端会返回一个订单号前端根据返回结果跳转到“订单列表页”或“支付页”。因为毕设一般不接入真实支付这里可以用一个“模拟支付”按钮代替点击后调用后端接口把订单状态从待支付改成待接单。4.3 Axios请求封装与权限控制两个前端工程都需要做Axios的封装这样做的好处是一处统一处理请求头、超时时间、错误提示和JWT注入。封装的思路大致如下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 if (res.code ! 200) { if (res.code 401) { // Token过期或未登录跳转登录页 router.push(/login) } Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res } error { Message.error(网络错误请稍后重试) return Promise.reject(error) } )这样封装完之后业务代码里只需要这样调用接口const res await request.get(/user/dish/list) this.dishList res.data.records无需在每一个业务请求里都手动处理Token和错误弹窗代码看起来干净很多。这个封装在用户端和管理端是通用的管理端额外在路由守卫里加一层beforeEach判断如果本地没有Token或者角色不是admin就重定向到登录页。4.4 前后端联调的常见问题前后端分离项目里联调阶段是最让人头疼的。这里我复盘一下最常见的几个问题跨域问题CORS。前端运行在8080端口后端运行在8081端口两个端口不同就属于跨域请求。后端要配置全局跨域支持具体做法我后面排错部分会给出完整代码。前后端字段不一致。比如后端Java实体类里是createTime驼峰命名前端JS里取data.createTime没问题但如果前端手滑拼成了data.createtime或者data.create_time界面上就显示undefined。原因是MySQL字段默认是下划线命名而Java实体是驼峰命名如果用了MyBatis-Plus需要在配置里开启驼峰映射mybatis-plus: configuration: map-underscore-to-camel-case: true但返回给前端的JSON字段名取决于实体类里的属性的命名。所以前后端要统一以Java实体或接口文档为准这是协作时最容易扯皮的地方。请求参数的格式问题。POST请求如果用的是Content-Type: application/json后端要用RequestBody接收如果用application/x-www-form-urlencoded则要用RequestParam接收。两种方式只能对应一种对不上就会报“Required request body is missing”之类的错误。关于Vue打包后布局异常的问题在热词里也出现了这里提一下。如果本地开发页面正常npm run build打包放到服务器后布局乱了大概率是静态资源路径配置问题。在vue.config.js里设置publicPath: ./让打包后的资源使用相对路径基本可以解决。5. 数据库初始化与系统部署全流程5.1 从SQL脚本到表结构落地拿到源码里的SQL脚本文件一般是restaurant.sql或init.sql第一步不是在Navicat里执行了事而是先看清楚有哪些表、表结构是什么样的、有没有初始数据。建议先把SQL文件用文本编辑器打开检查一下这几个点字符集和排序规则。表和应用建立连接时如果字符集不一致会出现中文乱码。建表语句里最好统一设置CREATE TABLE user ( ... ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci;用utf8mb4而不是utf8是因为utf8mb4是utf8的超集能完整保存emoji和生僻字是现在的主流方案。源码里的SQL如果是utf8也不用着急手动改成utf8mb4即可不影响表结构。导入的顺序。如果SQL文件里有外键约束那表的创建顺序是有讲究的——先创建被引用的主表再创建关联表。不过好一点的毕设源码一般没有繁琐的外键约束大多是逻辑关联。初始数据。管理员账号在哪张表里密码是什么形式存储的大部分毕设源码的管理员账号是明文存在user表里比如admin / 123456。如果是BCrypt加密的那你得看看源码里有没有提供初始化密码或者用代码注册一个。调试阶段我一般建议先在数据库里确认能拿到管理员信息再跑去前端登录页疯狂尝试能省不少时间。5.2 SpringBoot后端打包部署后端部署分两步打包和运行。首先用Maven命令打包mvn clean package -DskipTests执行成功后target目录下会生成一个restaurant-0.0.1-SNAPSHOT.jar文件这就是整个后端服务的可执行物。这里有个细节如果你的系统是JDK 8环境而pom.xml里配置的Java版本是17会导致无法打包或者打包后运行报UnsupportedClassVersionError。所以打包前一定要确认pom.xml里的java.version和机器上安装的JDK版本一致。如果版本不一致除了改pom还要改maven-compiler-plugin的source和target。然后启动服务最简单的运行方式java -jar restaurant-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod如果你的项目用了application.yml区分开发环境和生产环境就可以用--spring.profiles.activeprod来指定要加载application-prod.yml。生产环境的配置里要修改MySQL连接地址、用户名、密码还有文件上传的本地路径等。如果你只是在自己的Windows电脑上跑起来展示那直接用IDEA的Run按钮就行。但如果你是要部署到云服务器上或者论文里要写“系统部署在CentOS 7服务器上”那我建议用nohup方式启动nohup java -jar restaurant-0.0.1-SNAPSHOT.jar app.log 21 这样即使SSH断开服务也不会被终止。查看日志用tail -f app.log能实时看到SpringBoot的启动日志和报错信息。5.3 Vue前端打包与Nginx部署前端打包之前有一件事必须先确认后端接口的地址配置在哪里。如果按我前面说的方式封装的AxiosbaseURL是/api那打包后前端会向当前域名的/api路径发请求。本地开发时我们可以通过Vue CLI的devServer代理转发解决跨域// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样开发环境请求http://localhost:8080/api/user/login时Vue的devServer会自动转发到http://localhost:8081/api/user/login避免跨域。生产环境呢我们用Nginx做反向代理同样把/api路径转发到后端服务。前端打包npm run build打包完成后dist目录里就是纯静态文件。把这些文件上传到服务器的Nginx静态文件目录比如/usr/share/nginx/html/restaurant-web然后在Nginx配置文件里加一段server { listen 80; server_name your-domain.com; # 用户端静态页面 location / { root /usr/share/nginx/html/restaurant-web; index index.html; try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://localhost:8081/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意try_files $uri $uri/ /index.html;这一行非常关键。因为Vue Router使用了history模式如果用户直接访问/order/12这样的深层路由Nginx要确保能回退到index.html由前端路由接管否则会报404。如果你的前端用的是hash模式URL里带#那就不需要try_files但看起来没有history模式美观。我建议毕设展示时用history模式同时在论文里写一句“采用history模式实现美观的URL”。管理端同理把一个dist目录映射到另一个location路径下或者用不同的端口区分开。5.4 日常开发中的调试工具推荐除了Navicat管理数据库在前后端联调和问题排查时我特别推荐以下几个工具Postman或Apifox用来单独测试后端接口。比如你要验证登录接口是否正常不需要启动前端直接用Apifox发一个JSON请求看返回结果。这样可以把“前端问题”和“后端问题”快速隔离开来排查效率翻倍。Apifox还有一个优势可以基于接口自动生成接口文档这份文档在论文的“系统接口设计”章节里直接可用。Redis可视化工具AnotherRedisDesktopManager如果你在订单模块加了Redis缓存用它来查看缓存的key和value会方便很多。前端Vue开发必备的Vue Devtools浏览器插件可以看到组件的props、data、computed实时状态调试页面数据异常时非常有用。6. 如何把毕设源码转化成合格论文6.1 论文的结构与写作节奏很多人的误区是“先把代码写完再写论文”这会导致论文质量严重拉胯。因为题目、研究内容、核心设计在写代码时已经定型了论文只是事后补充记录逻辑上缺失了“需求分析驱动设计、设计驱动实现”的因果关系。所以我建议边写代码边记录设计决策代码写完初稿也就有了大半。毕业论文一般遵循这样的结构章节内容要点与项目的对应关系第一章 绪论研究背景、意义、国内外现状、论文结构找2-3篇同类系统论文提炼改进点第二章 相关技术介绍SpringBoot、Vue、MySQL、JWT等核心技术写清楚每个技术的版本和用途第三章 需求分析功能需求、非功能需求、用例图对应系统的角色和功能设计第四章 系统设计架构设计、数据库设计、接口设计对应分层架构和ER图第五章 系统实现每个模块的实现截图和核心代码对应前端页面和后端代码第六章 系统测试功能测试用例表、测试结果对应Postman或Apifox的接口测试记录第七章 总结与展望完成的工作、不足、未来改进方向客观描述强调学习收获这里我特别提醒一下“相关技术介绍”这一章。很多人直接抄技术文档大段大段地介绍SpringBoot是什么、Vue是什么结果答辩时老师问“你们的系统里用到了JWT它和传统的Session有什么区别”就答不上来。对策是技术介绍不要超过500字但要结合自己系统的实际用法来写。比如写Vue时谈到“系统的购物车模块使用了Vue的computed计算属性”这就把技术学习和项目实践绑定在一起了。6.2 需求分析的用例图怎么画需求分析章节的核心产出物是用例图和用例表。网上点餐系统的用例图很简单三个角色三条线注册用户登录、浏览菜品、管理购物车、下单支付、查看订单、管理地址商家/管理员登录后台、管理菜品、处理订单、查看统计系统管理员管理用户、管理分类画用例图常见的工具是StartUML、ProcessOn或者draw.io导出成PNG插入Word即可。画图时注意用例之间的包含关系、扩展关系不要画得太密否则图会很乱。用例表的形式也值得用表格列出来比如用例编号用例名称参与者前置条件基本流程后置条件UC-01用户登录注册用户已注册账号输入用户名密码系统校验通过生成Token返回用户信息进入首页UC-02提交订单注册用户已登录购物车有菜品已选地址点击提交订单系统校验库存生成订单号和订单记录清空购物车跳转支付页用例表的粒度不要太细抓住核心业务即可一般在10-15个用例之间。6.3 数据库设计章节的ER图与表说明数据库设计这一章是评委老师最爱提问的地方。内容上至少要包含两部分ER图和主要表结构说明。ER图表达的是实体之间的关联关系。画法上用户、菜品、订单、订单明细这四个核心实体之间的关系是用户和订单一对多一个用户可以有多条订单订单和订单明细一对多一个订单包含多个菜品明细菜品和订单明细一对多一个菜品可以出现在多个订单明细中但订单明细通过快照字段存了菜品名称和价格这里一定会被问到的一个问题是“订单明细表里为什么要有冗余的菜品名称和价格字段”回答的思路我在前面已经讲过了核心逻辑是历史快照保证价格变化后历史订单仍可追溯。表结构说明一般形式是一张表一段话配一张字段表格。不用把所有字段都列出来重点挑有设计考量的字段讲。比如订单表的状态字段就要说明取值范围0-5以及每个值对应的业务状态。6.4 测试章节不注水系统测试章节是毕设论文里水分最大也最好混的章节但好混不代表你应该随便编数据。正确做法是将测试记录和测试截图一一对应起来。功能性测试采用黑盒测试。比如“提交订单”这个用例测试步骤是用户登录系统将3份鱼香肉丝加入购物车进入结算页选择收货地址点击“提交订单”核对返回的订单号为xxxxxxxxxxxx到订单列表页面确认该订单状态为“待支付”对应的预期结果是“生成订单再跳转支付页”实际结果写“与预期结果一致”测试结论“通过”。性能测试部分如果不是专门做性能优化的同学简单测试一下并发下单即可。比如用JMeter模拟20个用户同时请求菜品列表接口查看TP99和错误率。这里不是为了测出性能瓶颈而是体现你有一定的工程意识。7. 常见问题与排错实录7.1 跨域请求报错CORS错误前后端分离开发模式下遇到最多的就是跨域问题。当你在浏览器控制台看到类似Access to XMLHttpRequest at http://localhost:8081/api/user/login from origin http://localhost:8080 has been blocked by CORS policy的报错时就说明后端没有正确配置跨域。解决方案在后端加一个CorsConfigurationSpringBoot下最简洁的写法是Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有一个很容易踩的坑如果配置了allowCredentials(true)那么allowedOrigins不能写成*要写具体的域名或用allowedOriginPatterns(*)。如果前端Axios又额外设置了withCredentials: true两边不一致也会报错。还有一个隐蔽的问题是如果拦截器先于CORS过滤器的顺序生效前端发起的OPTIONS预检请求会被拦截器拦截导致跨域没报错但登录请求始终失败。解决方法是让CORS过滤器先于JWT拦截器执行可以通过filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE)来调整过滤器顺序。7.2 数据库连接失败Access denied for user后端启动时报Access denied for user rootlocalhost (using password: YES)这个错误百分之九十是application.yml中的数据库账号密码写错了或者MySQL中确实没有这个账号。检查顺序打开application.yml确认url、username、password三项用Navicat或者命令行测试一下同样的账号密码能否正常连接检查数据库URL后面是否需要加参数?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai时区参数不加有时候会报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized的错误7.3 端口号占用Port 8080 was already in use开发环境启动后端时如果报Port 8080 was already in use说明8080端口被其他进程占用了。用以下命令查看到底是谁占用的# Windows netstat -ano | findstr 8080 # 然后根据PID结束进程 taskkill /pid xxx /f更省事的方法是直接改后端默认端口在application.yml里加server: port: 8081这样既避免了端口冲突又方便在Vue的devServer代理里指定目标地址。7.4 前端路由刷新404问题这个问题的表现是在Vue应用里点来点去一切正常但按F5刷新或者手动输入网址回车页面就白屏或者返回404错误。原因和我在Nginx配置里提到的一样——history模式下服务器不知道如何处理/order/12这种前端路由会按后端路由去找资源自然找不到。解决方案有三种后端SpringBoot加一个转发规则把所有非接口请求转发到index.html用Nginx的try_files $uri $uri/ /index.html;前端改用hash模式对毕设而言方案1和方案2任选其一即可。如果论文里写了系统上线部署那方案2正好配合部署章节一起呈现。7.5 npm安装依赖特别慢或者报错前端工程执行npm install时卡进度条或者报ERESOLVE unable to resolve dependency tree这种依赖树冲突错误。第一个问题优先换国内镜像源npm config set registry https://registry.npmmirror.com依赖树冲突多半是Node.js版本和Vue CLI版本不匹配。Vue 3项目建议Node.js 16.x以上Vue 2项目用Node.js 14.x即可。推荐用NVMNode Version Manager来管理Node版本随时切换。7.6 时间字段相差8小时服务器部署后页面上显示的时间比本地时间早了8小时或者插入数据库的时间不对。这是时区问题。数据库连接URL里加上serverTimezoneAsia/Shanghai同时MySQL服务器端的时区也要设为08:00基本就能解决。更稳妥的方式是统一规范所有时间字段的读写都遵循北京时间展示时也不要做额外换算。写在最后的经验之谈做这个项目几轮迭代下来我最大的感受是毕业设计的核心不是你“用了多少高深的技术”而是你“系统性地解决了一个真实场景下的完整问题”。一套网上点餐系统从需求拆解、数据库建模、后端业务逻辑、前端页面联调、打包部署再到论文成稿和答辩准备这一整个链条走下来你收获的绝不只是一个“优秀毕业设计”的头衔而是一整套工程化的思维方式。如果你拿到的源码不是自己写的建议你第一步不是急着跑起来而是先做两件事一件是把数据库ER图画出来看看自己能不能讲清楚每一张表的职责另一件是画一遍系统的功能结构图从登录注册到下单支付再到后台管理把每一个模块背后对应的代码文件关联起来做到评委随机点一个功能位置你都能快速跳转到对应代码并且解释两到三行核心逻辑。能做到这个程度答辩通过基本稳了。如果有余力的话可以给系统加一个很简单的优化点比如用Redis缓存菜品列表减少数据库压力或者为订单模块加一个Spring定时任务把所有超时未支付的订单自动取消。不需要复杂能体现“我不仅会写CRUD还知道系统上线之后会遇到什么问题”的工程思维就够了。当年我带过的学生里有一个在论文致谢里写了一句话让我印象很深“写代码的日子是琐碎的但把琐碎拼成完整系统的过程本身就是一种训练。”这话放在网上点餐系统这个题目上再合适不过。