又到了毕业设计选题的时候。每年这时候我都会收到不少同学的消息,问做什么题目既好实现又不容易翻车。如果你想选一个Web方向的题目,又希望前后端技术都能覆盖到,基于SpringBoot和Vue的早餐点单系统是个很稳的选择。
这个系统说白了就是一个面向学校食堂、写字楼周边早餐铺子的在线点餐平台。用户用浏览器打开页面,选好豆浆油条煎饼果子,下单后商家在后台接单、出餐,整个过程比现场排队效率高一大截。作为毕业设计,它的业务逻辑不会复杂到失控,又足够撑起一个完整项目的框架,用来展示你的工程能力非常适合。
下面我会把这个项目的整体设计思路、核心模块实现、常见坑点,以及答辩准备,一整套讲清楚。无论你是准备自己从零写一个,还是想找个参考项目,这篇文章都能帮你少走很多弯路。
1. 项目整体设计与需求拆解
1.1 核心业务场景与用户角色
做毕业设计最容易犯的错就是一上来就建表、写接口,结果做到一半发现业务漏洞百出。先把场景和角色定义清楚,后面会省非常多事。
早餐点单系统常见的角色有三种:普通用户(买早餐的顾客)、商家(早餐店老板或店员)、管理员(系统维护方)。这三种角色对应完全不同的使用诉求:
- 普通用户关心的是“怎么选餐最快、下单后多久能取”。所以前端页面必须够轻,菜单分类要清晰,下单流程要短,最好三步内完成:选餐、确认、提交。
- 商家关心的是“订单来了能不能快速看到、出餐状态怎么改”。所以后台要有一个订单工作台,能实时刷新订单列表,能一键切换订单状态。
- 管理员关心的是“数据乱不乱、用户和商家怎么管”。平台侧需要有基本的用户管理、店铺审核、统计报表能力。
我建议你把“早餐高峰期”这个场景先画下来:早上七点半到八点半,学生赶课、白领赶地铁,大量订单同时涌进来。如果系统在这一小时里能稳定跑通“用户下单-商家接单-出餐完成”,整个项目的主心骨就有了。所以在设计阶段就要保证订单列表的加载速度,以及商家工作台的新订单提醒不能延迟太久。
这三种角色的诉求不是平均分配的。作为毕业设计,用户端可以做得丰满一点,因为这是演示时的门面;商家端做扎实一点,因为订单流转是核心业务闭环;管理端功能点到即止,有基本的CRUD和统计即可,避免把自己拖进无底洞。
1.2 技术选型背后的思考
为什么选SpringBoot + Vue这个组合?不是因为烂大街,而是因为它最适合这个体量的项目。
后端用SpringBoot,最大的理由是“约定大于配置”。你不用像在SSH框架时代那样写一堆XML配置,一个启动类加几个注解,项目就能跑起来。对于毕业设计周期来说,这个优势是致命的——别人还在配环境的时候,你已经能把接口调通了。
前端用Vue,同样是因为门槛适中、生态成熟。Vue的语法比React更贴近传统HTML思维,模板写法直观,组件化开发又能体现设计感。配合Element Plus这种组件库,页面能快速做得干净整齐,答辩演示时第一印象分直接拉满。
这里有一个很多人忽略的选型原则:技术栈不要太冷门。答辩老师不一定会深入看代码,但一定认识主流技术。SpringBoot和Vue是招聘网站上的高频词,你写在简历上、写在论文技术选型章节里,都有说服力。
JPA还是MyBatis Plus?我的建议是MyBatis Plus。虽然JPA写起来更省事,但MyBatis Plus的SQL可控性更好,答辩时被问到“某个查询怎么写”你能讲得清楚。而且MyBatis Plus自带分页插件和条件构造器,对订单列表这种场景非常友好。
数据库就选MySQL,理由不用多说,稳定、资料多、面试也常问。服务器可以用宝塔面板部署,演示前把项目打成Jar包扔上去,外网一访问,效果很直观。
2. 核心功能细节与数据库设计
2.1 功能模块拆解
早餐点单系统的功能模块,按角色拆能拆出下面这些:
用户端:
- 注册登录:手机号或用户名注册,登录后携带Token访问接口。
- 菜单浏览:按分类看早餐列表,支持模糊搜索(比如搜“豆浆”)。
- 购物车:加购、减购、清空,购物车数据存在后端数据库,这样换设备也能同步。
- 订单管理:下单、查看订单列表、查看订单详情、取消订单、确认收货。
- 个人信息:修改头像、修改密码、查看历史订单。
商家端:
- 店铺信息维护:店名、营业时间、公告。
- 商品管理:早餐的上架、下架、价格修改、库存设置。
- 订单工作台:新订单提醒、接单、出餐、完成。
- 简易统计:今日订单量、今日营业额。
管理端:
- 用户管理:查询、禁用账号。
- 商家管理:审核开店申请、下架违规店铺。
- 订单总览:查看全平台订单。
- 数据统计:订单量趋势、销售额TOP商品。
这些功能看着多,但彼此之间的逻辑是主线清晰的:用户下单 -> 商家接单 -> 出餐完成 -> 订单归档。你只要把“订单状态机”这一条线想明白,其他功能都是围绕这条线的增删改查。
订单状态机是这个项目的灵魂。我建议状态字段用整数定义,并在代码里写成常量或枚举:
| 状态值 | 状态含义 | 触发动作 |
|---|---|---|
| 0 | 待支付 | 用户提交订单后生成 |
| 1 | 已支付待接单 | 用户完成支付 |
| 2 | 商家已接单 | 商家点击接单 |
| 3 | 出餐中 | 商家点击开始制作 |
| 4 | 待取餐 | 商家点击出餐完成 |
| 5 | 已完成 | 用户确认取餐/系统自动完成 |
| 6 | 已取消 | 用户取消或超时未接单 |
这里要特别注意,状态为6的取消要区分“用户主动取消”和“超时未接单系统取消”。两种取消的后端处理逻辑不一样,前者要释放商品库存,后者还要给用户一个提示。这个细节做好了,写进论文里就是业务逻辑完整性的体现。
2.2 数据库表结构与关系设计
数据库设计遵循一个原则:宁可多建表,不要一表扛万物。早餐点单系统的核心表我建议这样设计:
- user表:用户表,字段有id、username、password(BCrypt加密存储)、phone、avatar、role(USER、SELLER、ADMIN)、shop_id、status、create_time。
- shop表:店铺表,字段有id、seller_id(关联商家用户)、shop_name、address、notice、status(0待审核、1营业中、2休息中)、create_time。
- product表:商品表,字段有id、shop_id、name、category、price、stock、image、status、sales。
- cart表:购物车表,字段有id、user_id、product_id、quantity、selected。
- orders表:订单主表,字段有id、order_no、user_id、shop_id、total_amount、status、remark、create_time、pay_time、finish_time。
- order_item表:订单明细表,字段有id、order_id、product_id、product_name、product_image、price、quantity。
- address表:收货地址表,字段有id、user_id、consignee、phone、detail、is_default。
下单时为什么要把商品名称和价格冗余到order_item里?因为商品价格随时可能变动,如果订单明细只存product_id,之后商品改价,历史订单的价格就失真了。这个冗余是必须的,答辩时老师问起来,一定要能说出这个理由。
再看一个细节。order_no订单号建议用“日期 + 随机字符串”生成,比如取年月日时分秒拼接四位随机数。不要直接用自增id当订单号,一是容易被别人遍历猜测订单量,二是线下沟通时数字不友好。还有一点,订单状态字段必须建索引,因为订单列表页经常要按状态过滤,没有索引的表数据一多就会慢。
3. 关键模块的实操实现
3.1 后端SpringBoot核心配置与接口设计
先搭工程骨架。我用的是Maven + SpringBoot 2.7.x,Java 8。为什么不用Java 17或者SpringBoot 3.x?稳定性是第一位。SpringBoot 2.7的第三方资料最多,遇到报错搜索引擎一抓一大把;SpringBoot 3.x虽然新,但对JDK版本、对部分兼容库都有要求,毕业设计的场景没必要冒这个险。
pom.xml里必要的依赖包括:spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-j、lombok、spring-boot-starter-validation,以及JWT相关的jjwt依赖。Redis如果你要加,考虑部署成本和答辩演示的稳定性,可以先不加,把购物车存MySQL表里也完全够用。
application.yml的核心配置大概长这样:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/breakfast?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有一个很关键的操作:把MyBatis Plus的SQL日志打开。开发阶段一定要开着StdOutImpl,否则SQL写错了你只能看到一堆莫名其妙的报错,完全不知道数据是怎么查出来的。等要部署上线再关掉。
接口设计遵循RESTful风格,但不用太钻牛角尖。核心接口这组就够:
- POST /api/user/login 登录,返回JWT
- POST /api/user/register 注册
- GET /api/product/list 按店铺查商品列表
- GET /api/product/detail/{id} 查商品详情
- POST /api/cart/add 加购物车
- GET /api/cart/list 查购物车
- POST /api/order/create 创建订单
- GET /api/order/list 查订单列表
- POST /api/order/status 商家更新订单状态
Controller层的代码风格也要统一。以一个创建订单的接口为例:
@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/create") public Result<OrderVO> create(@RequestBody @Valid OrderCreateDTO dto, @RequestAttribute("userId") Long userId) { OrderVO vo = orderService.createOrder(userId, dto); return Result.success(vo); } }这里有两个设计点比较重要。第一,DTO和VO要分开,前端传进来的参数用DTO接收并加校验注解,返回给前端的数据用VO包装,不要把数据库实体直接往外暴露。第二,用户ID不是前端传的,而是拦截器解析Token后放到RequestAttribute里的,这样接口就没法伪造用户身份了。
统一返回结果类我建议用Result,包含code、message、data三个字段。code用200表示成功,其他是业务错误码。很多同学喜欢用“1表示成功0表示失败”,但HTTP语义里0/1太容易被误解,还是统一用200更规范。统一返回类的意义不只是好看,前端可以写一个全局的响应拦截器,code不等于200直接弹错误提示,代码能省下一大截。
JWT鉴权的实现思路:登录成功生成Token,前端每次请求在Header里带Authorization: Bearer xxx。后端写一个拦截器,校验Token并解析出用户ID放到RequestAttribute里。这里一定要记得处理Token过期的情况,一般设置24小时过期,过期后前端跳回登录页。拦截器配置时注意:/api/user/login和/api/user/register要放行,其他接口都拦。
3.2 前端Vue页面结构与请求封装
前端我用Vue 3 + Vite + Element Plus。Vue 3现在是最主流的选择,Vite启动速度快,开发体验比Webpack高一个档次。Node版本要注意,Vite 5要求Node 18以上,装之前先检查一下node -v,版本不对直接装不上依赖。
页面结构上,用vue-router做路由拆分:
- /login 登录页
- /home 首页(菜单列表)
- /cart 购物车页
- /order/list 订单列表页
- /order/detail/:id 订单详情页
- /shop/manage 商家后台
- /admin 管理后台
每个页面一个vue文件,放views目录,组件能拆的拆到components。比如商品卡片ProductCard可以复用两次,首页列表和搜索结果页都用到它。这就是组件化的价值,展示给老师看的是代码组织能力。
请求封装是前端的一个重点。建议在src/utils/request.js里基于axios封装一个实例:
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) 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) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('请求失败,请稍后重试') return Promise.reject(error) } ) export default request这个封装的妙处在于,页面里所有接口调用都变成一行:const res = await request.get('/order/list')。异常统一处理,代码极其清爽。答辩演示时,哪怕某个接口挂了也能优雅提示,不会出现一整页白屏这种尴尬场景。
登录页是整个系统的入口,虽然看着简单,但要把它写“稳”。常见的坑是表单校验不完整,用户没输入手机号点登录,后端包装了一堆空值处理。我的习惯是前端用Element Plus的表单校验规则必填,后端DTO也加@NotBlank注解,两道防线都装上。前端校验是为了用户体验,后端校验才是安全边界,这个思路要贯穿所有接口。
Vue页面开发的三个小技巧也顺带说一下。第一,列表页加载数据建议放在onMounted里,但要注意竞态,可以用一个loading标志位控制。第二,购物车加减数量时不要每次都请求后端,先在本地改数量,最后统一提交,体验会好很多。第三,商家工作台的订单刷新用setInterval轮询,每5秒拉一次新订单,比WebSocket简单得多,毕设完全够用。
3.3 前后端联调与权限控制
前后端联调中最常踩的坑是跨域。如果你把前端跑在Vite的5173端口,后端在8080端口,跨域是必然的。解决办法有两个:一是后端加一个CORS配置类,允许指定来源;二是用Vite的proxy代理,把/api开头的请求转发到8080。
我更推荐第二种,因为生产环境部署时前后端同域,改造成本最小。具体配置在vite.config.js里:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这样前端代码里所有请求都写相对路径/api/xxx,本地联调用代理转发,部署到服务器后让Nginx把/api也反代到后端,前后端代码一行都不用改。Nginx部署时的关键配置长这样:
server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里try_files那行特别重要,Vue的history路由模式下,如果用户直接访问/order/list这样的地址,Nginx找不到对应文件就会404,必须让它回退到index.html。这个方案完整走通后,答辩的时候可以主动讲出来,老师们会很认可。
权限控制这块要分两步走。第一步,后端拦截器校验Token,确保未登录用户访问不到订单、购物车这些接口。第二步,前端路由加一个简单的守卫,未登录访问受保护页面时强制跳登录。前端守卫只是体验优化,真正的安全边界必须放在后端,这个安全认知一定要讲给答辩老师听。
另外,商家和管理员的权限要区分。用户表里的role字段帮我们实现这个需求。后端可以写一个@RequireRole注解加AOP拦截,或者简单点,在拦截器里解析出role再判断接口权限。我建议用注解的方式,因为代码更工程化。比如@RequireRole("SELLER")加在商家接口上,访问时拦截器检查角色,不匹配就返回403。优雅是一回事,更重要的是这个设计模式在面试里也常被问到。
4. 常见问题与排查技巧
4.1 典型问题速查表
做这个项目会遇到的坑,我闭着眼睛都能列出来。先给一张速查表,遇到问题先对号入座:
| 现象 | 常见原因 | 解决思路 |
|---|---|---|
| 前端请求报404 | Nginx或Vite代理没配好,后端路径不对 | 检查代理配置与Controller的RequestMapping |
| 登录一直失败 | 密码没有BCrypt加密,或加密方式不一致 | 确认注册和登录使用同一套加密逻辑 |
| 订单状态不更新 | 前端轮询没启动,或接口调用成功但没刷新数据 | 打印后端日志确认接口被调用了没 |
| 数据库中文乱码 | 连接串缺少characterEncoding=utf8 | 修改application.yml中的URL参数 |
| 部署后前端白屏 | 静态资源路径不对,或history路由未配置 | 改用hash路由,或Nginx配置try_files |
| 端口被占用 | 本地有其他进程占用8080 | 换端口或用lsof/netsh排查 |
这里我特别想展开讲一下404问题。前端请求报404,很多人第一反应是接口没写对,但在前后端分离项目里,80%的情况是代理没生效。先用浏览器开发者工具看Network面板,确认请求URL是/api开头的相对路径,还是带端口的绝对地址。如果是绝对地址,说明代理配置没走到,优先检查vite.config.js有没有生效,改了配置记得重启dev server。
4.2 实战避坑经验
第一个大坑是MyBatis Plus的自动填充失效。我给orders表的create_time字段配置了MetaObjectHandler自动填充,但批量插入的时候发现时间没填上。排查了一圈才发现,自动填充需要字段上有@TableField(fill = FieldFill.INSERT)注解,并且插入时字段不能显式设为null。解决办法就是老老实实给实体类加注解,不要想当然。
第二个坑是金额计算精度。早餐订单里价格都是小数,如果数据库用float或double存,浮点误差会在结算时悄悄埋雷。比如3个1.5元的包子,float加起来可能是4.4999999。我的经验是:数据库amount字段一律用decimal(10,2),Java实体用BigDecimal,计算时用BigDecimal的add方法而不是直接相加。这个细节写论文时也可以当亮点单列一节。
第三个坑和跨域有关。后端配置CORS时如果允许了所有来源且带了allowCredentials(true),浏览器会直接拒绝请求。这是CORS规范里的一个安全限制:带凭证的请求不能允许所有来源。如果你的前端需要带Cookie,origin必须写明确地址,不能写*。虽然我们项目用Token不用Cookie,但这个坑值得记下来,省得以后换了场景还要再踩。
第四个坑是部署环境的服务内存不够。SpringBoot默认启动参数在云服务器上1G内存会有点吃力,跑起来很卡。部署时可以这样优化:打包时用mvn package -DskipTests,启动时加-Xmx256m -Xms128m限制JVM内存。还有把多余的服务都关闭,比如MySQL和后端不要同时堆在同一台小机器上。我用2G内存的服务器实测,这套系统跑起来没压力。
5. 答辩准备与项目扩展
5.1 毕业设计答辩常见提问
答辩环节,老师最喜欢问的问题其实就那么几类。提前准备好,现场就不慌。
第一类:为什么选择这个课题。不要只回答“早餐点单系统有市场需求”这种万能句。你可以具体一点:观察到学校食堂早餐高峰期排队严重,人工点餐效率低,基于Web的提前点单可以缩短等待时间。这个回答既有场景又有针对性。
第二类:技术选型问题。老师会问“SpringBoot和传统SSM比优势在哪”“为什么前端选Vue不选React”。回答思路是:SpringBoot简化配置、内嵌Tomcat、生态成熟,适合快速搭建RESTful服务;Vue响应式数据绑定和组件化适合快速开发管理界面,而且与后端JSON数据天然契合。这些都是实话,讲到点子上就行。
第三类:系统安全性。问法通常很直接,比如“你的系统怎么防止别人非法访问”或者“用户密码存了明文吗”。回答要点:密码用BCrypt加密存储、JWT做身份认证、拦截器校验Token、接口按角色权限控制、统一异常处理防止堆栈信息泄露。这套说完,老师基本满意。
第四类:数据库设计。常见问法是“订单表为什么拆成主表和明细表两张”。回答要点:一对一从表结构上解决不了“一个订单多个商品”的存储问题;拆开后订单明细可以记录当时的商品快照价格,历史订单不会因为商品改价而失真;统计报表直接查明细表很灵活。
第五类:系统怎么测试的。别只答“我测过了”。说清楚你测了什么:功能测试覆盖登录、下单、接单、状态流转全流程;接口用Postman验证了状态码和返回结构;异常场景测了库存不足、未登录访问、Token过期。有具体操作过程的回答,才有说服力。
建议你答辩前把项目跑起来,从注册一个用户、选几个商品、下完一单,到商家端接单出餐,完整走一遍流程。很多时候演示卡壳不是因为程序bug,而是因为业务流程没记熟。
5.2 项目后续扩展方向
如果毕业答辩之后你还想继续完善这个项目,有几个扩展方向性价比很高。
第一个是引入Redis做缓存和分布式会话。把商品分类和热销榜单缓存到Redis,购物车也可以迁到Redis,登录Token改成Redis维护,这样服务的响应速度和并发能力会有明显提升。这也是面试时能拿出来讲的加分项。
第二个是加入微信小程序端。学生吃早饭的场景,很多用户更习惯用小程序而不是浏览器。SpringBoot后端接口设计好了的话,新增一个小程序前端并不难。复用现有RESTful接口,再适配一套微信小程序的UI,整个项目就从Web端延伸到移动端了。
第三个是引入消息通知。订单状态变化时,通过短信或者站内信通知用户。技术实现可以用SpringBoot的事件机制,或者引入消息队列做解耦,这样项目在架构描述上又能多一个层次。
第四个是完善数据分析功能。早餐是高频小额消费,数据特别适合做统计。可以增加一个经营看板,展示每日销量趋势、各商品销售量占比、复购率。这部分做好了,项目就从“管理工具”变成了“经营工具”,立意完全不同了。
从选题到部署,这个项目我完整走了一遍,最深的体会是:一个系统的价值不在于用了多少新技术,而在于业务逻辑是否跑得通、关键设计能否讲清楚。你只要把订单状态机、权限控制、数据表关系这三件事想明白,毕业设计拿到一个不错的成绩基本没有悬念。项目做完之后也别扔到角落,把它整理好放到简历上,面试的时候大方地讲一讲你踩过的坑和你的设计思路,那才是这段经历真正的回报。