简介:基于Spring Boot与Vue构建的校园奶茶店线上点单系统,是面向计算机专业期末大作业、毕业设计或Java全栈实战练习的完整项目。资源涵盖前端点单、后台管理、订单处理等核心模块,配套MySQL数据库脚本与设计报告文档,可帮助学习者快速理解前后端分离开发流程并直接运行验证。包体共2000个文件,整体17.02MB,主要包含java后端源码、vue页面组件、js交互逻辑、css样式、ts类型定义及sql初始化脚本等,目录结构清晰,便于按模块查阅与二次开发。项目曾获导师指导认可并取得98分高分,所有源码均经本地编译调试确保可运行,附带的报告文档亦可作为期末报告或论文撰写的参考。目前已有20人学习下载,适合需要可靠项目模板的在校学生与初学者。
1. 校园奶茶店点单系统:期末大作业里性价比最高的一条路
期末大作业最怕的不是题目难,而是题目看起来高大上、到手却发现既跑不起来又讲不清楚。基于SpringBoot+Vue的校园奶茶店线上点单系统,恰好卡在一个很舒服的位置:前端有 Vue 页面可展示,后端有 SpringBoot 接口可追问,数据库有订单和商品表可画 ER 图,报告文档还能把下单流程写成用例。整套东西覆盖了前后端分离、增删改查、状态流转这几个答辩时一定会被问到的核心点。适合 Java 方向学生、需要交课程设计的同学,以及想快速搭一个能演示的 Web 项目的人。这篇文章把它从数据库到前端打包完整拆一遍,照着能跑,跑完能讲。
2. 系统拆解:先定功能边界,再定数据表,最后定订单状态
动手写代码前最忌直接开 IDEA 敲类。校园奶茶店点单系统的核心不是“能注册能登录”,而是“点单—支付—制作—取单”这条链路能不能闭环。我一般会先把功能边界画出来,再设计数据库,最后才写接口。
2.1 功能边界:不做支付,不做库存预警,只做点单闭环
期末大作业的场景是校园奶茶店,不是美团。常见的功能清单是:用户注册登录、浏览商品分类、加入购物车、提交订单、查看订单状态、商家后台修改订单状态、用户取消订单。至于在线支付、库存自动扣减、优惠券、会员积分,这些属于加分项,不属于核心链路。如果一开始把支付接进来,沙箱环境配置、回调逻辑、对账表设计会吃掉大量时间,而且答辩时老师大概率会追问“你的支付安全怎么保证”,自己反而答不上来。
所以我会把系统切成两个角色:用户端和店家端。用户端能点单、能看自己的订单列表、能取消待接单的订单;店家端能看到所有订单、能把订单状态从“待接单”推到“制作中”再到“待取”。不需要做复杂的权限框架,用 SpringBoot 的拦截器判断一下登录状态,再判断一下 isAdmin 字段就够了。这样既讲了权限控制,又不会掉进 Spring Security 的配置深渊。
2.2 数据库五张核心表:字段设计直接决定报告有没有东西可写
数据库是报告里最好写、也最容易扣分的地方。很多人的库表只有 user 和 order 两张表,字段也乱,比如订单金额用 double 而不是 decimal,时间用字符串而不是 datetime。我会用五张表撑起整个业务:user、category、product、orders、order_item。
user 表存用户和店家标记,字段有 id、username、password(加密后的)、phone、is_admin、create_time。category 表就是奶茶分类,比如“招牌奶茶”“鲜果茶”“纯茶”。product 表挂在 category 下,包含 name、price、image、status、description。orders 表是核心,包含 order_no、user_id、total_amount、status、create_time、pay_time、finish_time。order_item 表把一次订单里的多个商品拆成多行,包含 order_id、product_id、quantity、price。这里最需要注意的是 total_amount 不能冗余在 order_item 里,而是下单时算好总额写进 orders 表,后面只读不改。
字段类型上,价格一律用 decimal(10,2),状态用 tinyint 加注释,时间用 datetime。订单号不要用自增 id,因为答辩时老师会问“订单号怎么生成的”,我用的是时间戳加用户 id 加随机数,虽然丑但讲得清楚。外键要不要建?实际操作中我习惯不加物理外键,只在逻辑层维护,否则 Navicat 导数据、删数据时会被外键约束卡住,期末答辩现场可没有时间排错。
2.3 订单状态机:用一个 status 字段撑起整个流程
订单状态是这个系统的灵魂。我见过把订单状态做成“已支付”“未支付”两个枚举就交差的,但老师一问“用户下单后你如何判断店家有没有接单”,就卡住了。合理的最小状态集是:0 待支付、1 待接单(已支付)、2 制作中、3 待取、4 已完成、5 已取消。用户下单后如果模拟支付,直接调到 1;店家接单后从 1 变 2;制作完成变 3;用户取单变 4。用户只能在 0 或 1 状态取消,店家只能在 1 状态接单,这些规则要写在 Service 层,不能只靠前端按钮隐藏。
为什么不用删除订单来实现取消?因为报告里可以写“为保证数据可追溯,订单状态采用流转而非物理删除”。这句话在答辩时很加分。状态变更的时间点也要记录,比如 pay_time、finish_time,这样报告里能画一条“订单生命周期时序图”。状态机设计好之后,后端的接口逻辑就变得很简单了,本质上就是“查订单、判断当前状态、改成下一个状态、更新时间”。
3. 后端落地:SpringBoot 分层搭建与订单接口实现
后端部分我用的结构是标准的 Controller-Service-Mapper 三层,相信这也是课程里最常讲的分层。项目结构固定下来后,新增功能无非是三步:写实体、写接口、写实现。下面我从创建项目开始,逐步把最核心的创建订单接口拆开讲。
3.1 项目结构和 pom.xml:版本选择直接决定你能不能跑起来
创建 SpringBoot 项目时,我强烈建议先确认 Spring Boot 版本。如果你用的是 IDEA 2023 之后自带的 Spring Initializr,默认很可能给你生成 Spring Boot 3.x,它把 javax 包迁移到了 jakarta,很多网上抄来的代码会直接报红。期末大作业求稳,我一般选 Spring Boot 2.7.x,对应 Java 8 或 Java 11,兼容性最好,网上资料也最多。
pom.xml 里核心依赖就四个:spring-boot-starter-web、mybatis-spring-boot-starter(或 mybatis-plus,看你自己习惯)、mysql-connector-java(注意 Spring Boot 2.7 的 groupId 是 mysql,不要写错)、lombok。其他像 spring-boot-starter-validation 属于可加可不加。如果你用的是 mybatis-plus,它自带了分页插件和 BaseMapper,能少写很多样板代码,但代价是答辩时老师问“SQL 怎么写的”,你说“不用写”,这反而可能减分。我自己的习惯是:用原生 MyBatis 加 XML 文件,每个接口都能看到 SQL,报告里也有的写。
关于 springboot 配置,application.yml 里数据库连接这一项是最容易出问题的。注意时区要写成 serverTimezone=Asia/Shanghai,驱动要写成 com.mysql.cj.jdbc.Driver,URL 里记得加 useSSL=false 和 characterEncoding=utf8,否则连数据库会警告甚至报错。端口默认 8080,但很多人的电脑上 8080 可能被占用,我习惯在 yml 里写成 8081,前端代理也对应改,省得现场翻车。
3.2 创建订单接口:事务、金额计算、库存扣减一个都不能少
点单系统的核心接口是“提交订单”,它涉及到同时写 orders 表和 order_item 表,必须用事务。我用原生 MyBatis 给你一个最小可跑的版本。
先看实体类:
// Orders.java public class Orders { private Integer id; private String orderNo; private Integer userId; private BigDecimal totalAmount; private Integer status; // 0待支付 1待接单 2制作中 3待取 4已完成 5已取消 private Date createTime; private Date payTime; private Date finishTime; }然后是 Mapper 接口:
// OrdersMapper.java @Mapper public interface OrdersMapper { int insert(Orders order); int updateStatus(@Param("id") Integer id, @Param("status") Integer status, @Param("finishTime") Date finishTime); List<Orders> selectByUser(@Param("userId") Integer userId); }真正的业务逻辑在 Service 实现里:
// OrderServiceImpl.java @Service public class OrderServiceImpl implements OrderService { @Autowired private OrdersMapper ordersMapper; @Autowired private OrderItemMapper orderItemMapper; @Autowired private ProductMapper productMapper; @Override @Transactional(rollbackFor = Exception.class) public Integer createOrder(OrderCreateDTO dto) { // 1.计算总金额 BigDecimal total = BigDecimal.ZERO; for (OrderItemDTO item : dto.getItems()) { Product product = productMapper.selectById(item.getProductId()); if (product == null || product.getStatus() == 0) { throw new RuntimeException("商品不存在或已下架"); } total = total.add(product.getPrice().multiply(new BigDecimal(item.getQuantity()))); } // 2.生成订单主表 Orders order = new Orders(); order.setOrderNo("OD" + System.currentTimeMillis() + dto.getUserId()); order.setUserId(dto.getUserId()); order.setTotalAmount(total); order.setStatus(0); order.setCreateTime(new Date()); ordersMapper.insert(order); // 3.插入订单明细 for (OrderItemDTO item : dto.getItems()) { OrderItem oi = new OrderItem(); oi.setOrderId(order.getId()); oi.setProductId(item.getProductId()); oi.setQuantity(item.getQuantity()); oi.setPrice(productMapper.selectById(item.getProductId()).getPrice()); orderItemMapper.insert(oi); } return order.getId(); } }这段代码的逻辑是:先在内存里根据商品表单价和数量算出 totalAmount,然后插入主表拿到自增 id,再循环插入明细表。整个过程放在一个事务里,任何一个明细插入失败,主表也会回滚,不会出现“订单存在但里面没商品”这种脏数据。这里故意没有扣库存,因为奶茶店场景是一个一个做的,库存约束不强;如果你想加,就在 product 表加一个 stock 字段,在循环里用 UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity},影响行数为 0 就抛异常。
参数说明:@Transactional(rollbackFor = Exception.class)这个注解必须写 rollbackFor,否则 Spring 默认只回滚 RuntimeException,而很多自定义异常是 Exception 的子类,不回滚就麻烦了。生成订单号我用的是"OD" + System.currentTimeMillis() + userId,单机够用;如果你觉得太粗暴,可以用UUID.randomUUID().toString().replace("-", "")的前 16 位。注意 BigDecimal 运算一定用 add/multiply,不要用数字相加,否则精度会丢。
3.3 订单状态更新接口:把规则写在代码里,不要信任前端
店家和用户都有修改订单状态的需求,但规则不同。我把这些判断写在 Service 方法里,而不是让前端把目标状态直接传进来。这样才能在答辩时说“后端校验了状态流转的合法性”。
@Override @Transactional(rollbackFor = Exception.class) public boolean updateStatus(Integer orderId, Integer userId, Integer targetStatus, boolean isAdmin) { Orders order = ordersMapper.selectById(orderId); if (order == null) { throw new RuntimeException("订单不存在"); } // 非管理员只能操作自己的订单 if (!isAdmin && !order.getUserId().equals(userId)) { throw new RuntimeException("无权操作该订单"); } int current = order.getStatus(); // 用户取消:待支付或待接单状态下可取消 if (targetStatus == 5) { if (current != 0 && current != 1) { throw new RuntimeException("当前状态不可取消"); } } // 店家接单:待接单 -> 制作中 if (targetStatus == 2 && current != 1) { throw new RuntimeException("只能接待接单的订单"); } // 店家出杯:制作中 -> 待取 if (targetStatus == 3 && current != 2) { throw new RuntimeException("制作完成后才能标记待取"); } // 用户取单:待取 -> 已完成 if (targetStatus == 4 && current != 3) { throw new RuntimeException("待取状态才能确认完成"); } // 模拟支付:待支付 -> 待接单 if (targetStatus == 1 && current != 0) { throw new RuntimeException("只有待支付订单才能支付"); } ordersMapper.updateStatus(orderId, targetStatus, targetStatus == 4 ? new Date() : null); return true; }这段代码就是把状态机翻译成 if 判断。每一条分支都对应报告里的一行用例,答辩的时候老师随便抽一个状态组合,你都能用这段代码来解释。注意这里有个细节:取单完成时把 finishTime 带上,方便报表统计。更新状态用的是通用updateStatus,只改 status 和 finish_time 两个字段,不影响订单其他内容。
4. 前端落地:Vue 项目搭建、axios 联调与打包进 SpringBoot
前端部分我会用 Vue 2 + Vue CLI 来写,因为 Vue 3 的生态虽然新,但期末大作业的模板案例、视频教程大多还是 Vue 2。如果你已经装了 Vue CLI,直接vue create milk-tea-front,选择 Manually select features,勾上 Router 和 Vuex,其他默认即可。vue 安装及环境配置这一块网上教程很多,这里说几个容易卡住的地方。
4.1 Vue 项目结构与点单页面核心逻辑
项目的结构我会分成 views、router、api、components 四个目录。views 里面放 Login.vue、Menu.vue、Cart.vue、OrderList.vue、AdminOrder.vue;api 目录下面统一放 request.js 和 order.js;router 里配置路由守卫,没登录跳转 Login;components 里放一个商品卡片组件。
点单页面的核心不是 UI,而是状态管理。我习惯用 Vuex 维护一个 cart 数组,每个元素是{ productId, name, price, quantity, image }。加入购物车就是往这个数组 push 或累加数量,提交订单时把这个数组发到后端。这样增删改查的逻辑在纯前端就能讲清楚,然后才是 axios 调用。
4.2 axios 封装和订单提交:把 request 拦截器写好,后面少写一百行
前后端联调最怕每个页面都写一遍axios.post,而且 token 到处散落。我一般会在 request.js 里做统一封装:
// api/request.js import axios from 'axios' import router from '../router' const request = axios.create({ baseURL: '/api', // 统一前缀,后端接口都挂在 /api 下 timeout: 10000 }) // 请求拦截器 request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }, error => Promise.reject(error)) // 响应拦截器 request.interceptors.response.use( response => { const res = response.data // 后端统一返回 { code: 0, data: ... },code 非 0 表示业务失败 if (res.code !== 0) { alert(res.msg) return Promise.reject(new Error(res.msg)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } ) export default request这段代码的作用是把所有请求统一加上 baseURL /api,自动带上本地存储的 token,并且把后端返回的业务码统一解开。注意后端 Controller 的 RequestMapping 前缀也要写成/api/order/create,两边对上才不会 404。拦截器里 401 跳转登录,省得每个页面都写 catch。
提交订单的 API 封装就很简单:
// api/order.js import request from './request' export function createOrder(items) { return request({ url: '/order/create', method: 'post', data: { items } }) }在 Menu.vue 里点“去结算”时,把 Vuex 里的 cart 传给 createOrder,成功后清空 cart 并跳转到订单列表。这一步是前后端联调的第一个里程碑:你能在浏览器里往 MySQL 里插进去一条订单。如果这一步通了,后面就是查漏补缺。
4.3 将 Vue 打包放进 SpringBoot:路径问题与刷新 404
期末交作业是要求一个能双击运行的可执行 jar 包,而不是让老师给你启动前端开发服务器。所以要把前端npm run build生成的 dist 目录里的文件,复制到 SpringBoot 的src/main/resources/static下。我一般用 install 插件或者直接手动复制,不建什么花哨的 CI。
复制好了之后,打开 jar 包访问http://localhost:8081/就能看到页面。但有一个高频坑:Vue 默认用的是 history 路由模式,打包后访问首页没事,一旦刷新/orderList这个路径,SpringBoot 找不到对应的静态资源,直接 404。解决方法是把 Vue 的路由模式改成 hash:
// router/index.js const router = new VueRouter({ mode: 'hash', // 不要用 history,期末作业没必要和部署较劲 routes })改成 hash 后,URL 会变成http://localhost:8081/#/orderList,刷新不会丢。如果你想保留 history 模式,就得在 SpringBoot 里写一个转发 Controller,把非 /api 开头的请求都转发到 index.html,这个操作不算难,但答辩解释成本高。我建议直接用 hash,稳。
另一个坑是打包后的静态资源路径。vue.config.js 里 outputDir 默认就是 dist,但 assetsPublicPath 在 Vue 2 里默认是/,意思是打包后的 js/css 引用路径是绝对路径/js/app.js,如果你的 jar 是放在根路径部署的没问题,但如果想放到/demo/子路径下访问,就必须改成相对路径。期末演示我在根路径跑,所以保持默认即可。
4.4 代理配置:开发时用 devServer 跨域,部署后不需要跨域
开发阶段前端跑在 8080,后端跑在 8081,直接 axios 请求 8081 会跨域。两个办法:一个是在 SpringBoot 里加 CORS 配置,另一个是让 Vue 的 devServer 代理。我更推荐代理,因为代码里不用写全路径。
// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } }这样前端开发时请求/api/order/create会被代理到http://localhost:8081/api/order/create,绕开了跨域。部署时前端文件已经放进 SpringBoot 的 static 目录,前后端同源,根本不存在跨域问题。所以看到这里你就明白,为什么 request.js 里 baseURL 写/api而不是http://localhost:8081/api——前者在开发和部署下都能用,后者上线就会死。
5. 常见问题排查:从数据库导入到打包部署的五个坑
期末大作业最崩溃的瞬间往往不是代码逻辑,而是环境问题。我结合平时的排错经验,把最容易在演示前一晚出现的五个坑列出来,每条都是“现象 → 原因 → 解决”。
坑一:Navicat 导入 SQL 报 “Unknown column” 或 “Table doesn't exist”现象:从别人项目里拿到的 sql 文件,打开执行后报错,或者表导进去了但字段对不上。原因:那份 sql 可能是高版本 MySQL 导出的,带了ALTER TABLE或CREATE TABLE的backticks,而你的 MySQL 版本较老;也有可能脚本里混着INSERT INTO数据但表结构没先建。解决:先手工建一个空库,字符集选utf8mb4,然后只执行 CREATE TABLE 语句,成功后再执行 INSERT 语句。如果 SQL 里有SET FOREIGN_KEY_CHECKS=0最好,没有就别纠结,把建表顺序按依赖排列。
坑二:SpringBoot 启动报 “Access denied for user 'root'@'localhost'”现象:后端一启动就报数据库连接失败。原因:application.yml 里的密码写错了,或者你的 MySQL 允许的 host 不是 localhost。解决:先用 Navicat 用同样的用户名密码试连一次,能连上再改 yml。注意 yml 里的密码不需要加引号,但是如果密码里有特殊字符比如@,一定要包在引号里。另一个隐蔽问题是 MySQL 8.x 默认的认证插件是 caching_sha2_password,老版本的 mysql-connector-java 驱动连不上,解决方法是把驱动升到 8.0.x,或者给用户改回 mysql_native_password。
坑三:Vue 打包后页面是空白的,F12 报错 “Failed to load resource: 404”现象:把 dist 文件复制进 static 后,打开首页一片空白,控制台看 js 文件 404。原因:vue.config.js 的 publicPath 是/,而你把项目放在sout/static下其实是用 jar 包根路径访问,但如果你把静态文件直接放在磁盘上的“static”目录双击打开,浏览器无法解析绝对路径。解决:确认你访问的是http://localhost:8081而不是file:///D:/.../static/index.html。放进 SpringBoot 后必须启动 jar,不能双击 html。
坑四:hash 模式下页面能显示,但切换路由后样式消失现象:从首页跳转到订单列表,页面刷新后只剩文字没有样式,或者 CSS 加载失败。原因:静态资源路径不对,CSS 文件用了绝对路径。解决:在 vue.config.js 里设置publicPath: './'(Vue 2),这样打包出来的 index.html 里引用的 js/css 都是相对路径,任何子路径下都能找到。重新打包再复制一次。
坑五:报告文档字数凑不够,只能复制代码凑字数现象:报告写了功能概述和代码,但距离字数要求还差两千。原因:没把数据库表结构、接口用例、页面截图转化成文档。解决:把五张表每个字段都列成一张表说明含义,把订单状态机的六种状态转换画成表格,再把每个接口的请求参数、响应结果、正常/异常场景各写一行用例。光这三部分就能轻松凑够三千字,而且全是老师爱看的内容。
提示:如果你用的数据库同步软件或导入工具是 navicat 或 Workbench,导入大 SQL 前先检查脚本里有没有
USE语句,否则可能导入到默认库,到时候到处找表找不到,白白浪费时间。
6. 从能跑到能答辩:三个加分项和一套验证方法
项目做到能跑只是及格线,答辩加分靠的是你能说出“为什么这么设计”。我每次给同学看项目,都会建议加三个小功能,加起来不超过半天,但效果立竿见影。
第一个是登录后的 JWT 令牌。不用接 Spring Security,自己写一个拦截器,解析 Authorization 头里的 token,解析失败就返回 401。答辩时老师一问你“怎么防止用户越权访问”,你就能掏出这段代码讲明白。第二个是操作日志。在接送单、取消订单这些写操作上加一个@LogAnn注解,用 AOP 把操作人、操作类型、时间、订单号存到一张 log 表里。不用做得很完美,能让老师看到“有审计功能”就够了。第三个是订单超时自动取消。起一个定时任务,每分钟扫一次订单表,把创建超过三十分钟且状态为 0 的订单改成 5。这三样东西加进报告,工作量不大,但能让项目从“课程练习”升到“有点工程意识”。
验证方法也很重要。我习惯在答辩前按这个顺序走一遍:启动 MySQL,确认 Navicat 能连上;启动 SpringBoot,看控制台没有红色报错;打开浏览器访问/,能出现首页;点一杯奶茶下单,看数据库 orders 表多了一行;店家端登录,点接单,再刷新用户端页面,能看到状态变了。这套流程跑一遍不超过五分钟,但能避免 90% 的现场翻车。
最后说说我的血泪经验:期末项目最大的坑不是代码,而是“代码在别人电脑上能跑”。曾经有个同学把项目拷给我,他那边一切正常,我这边启动就报 MySQL 字符集错误,查了半天是他的 SQL 脚本里写死了utf8,而我的库是utf8mb4。所以交作业前,一定要在另一台干净的电脑上,从建库、导脚本、改配置、启动、访问,完整走一遍。这个习惯救了我很多次。希望帮到你。
本文还有配套的精品资源,点击获取