简介:这是一份基于Spring Boot与Vue.js的网上点餐系统毕业设计项目源码包,面向计算机相关专业、正在准备毕业设计或课程设计的学生。项目已经导师指导认可,答辩评审分达97分,并在Windows10/11环境下经过严格调试,具备开箱即用的特点,部署文档和演示视频能帮助使用者快速进入运行状态。压缩包整体约66.37MB,包含前后端源码、数据库脚本、答辩PPT、毕业论文、使用文档及操作演示视频,从系统开发到论文撰写再到答辩展示均有对应资料支撑,整体结构清晰、配套内容完整,能有效节省资料整理和排错时间。目前已有206人学习下载,对于希望快速搭建网上点餐系统、积累Spring Boot与Vue.js项目经验的学生而言,是一套完整度较高的参考方案,尤其适合毕业设计、期末作业和答辩展示等场景。
1. 别急着解压:先把网上点餐系统当一套“完整开发流程”读
拿到“基于Springboot+Vue网上点餐系统源码+数据库+PPT+论文+使用文档+演示视频(高分项目).zip”这类压缩包时,很多人的第一反应是解压、导入 IDEA、改个标题和 Logo、把演示视频里的话术背一遍,然后直接放进毕业设计或简历作品集里。我见过不少这么操作的人,答辩时被问“订单表和订单明细表为什么分开”“下单接口怎么保证不超卖”“Vue 路由刷新后为什么白屏”就卡住,因为项目是别人的,代码只在“能跑”的层面属于你。
这个标题的真正价值不是那个高分结论,而是它完整覆盖了一个业务系统的闭环:Spring Boot 做后端接口与数据库交互,Vue 做管理后台和用户端页面,数据库里躺着菜品、订单、用户、购物车等一套可讲清楚的业务表,PPT、论文和演示视频则是把“你会不会做”转译成“你能讲明白”的交付物。适合的人群很具体:计算机相关专业正在做课程设计或毕业设计的学生,想练全栈但不想从零搭脚手架的初级开发,以及需要给团队快速搭一套内部点餐演示系统的工程师。接下来我从技术选型、数据库建模、核心接口实现、前端联调和最后的环境梳理五个层面,把它讲成一份你自己也能复现的方案。
2. Spring Boot + Vue 选型逻辑与数据库建模:先把表关系立住
2.1 为什么是 Spring Boot 而不是 Spring Cloud;为什么 Vue 2 + Element-UI 更稳
这是一个单人能在两周内做完、答辩时能讲清楚体量的小型业务系统,Spring Boot 就是比 Spring Cloud 合适。Spring Boot 内置 Tomcat,java -jar 一条命令就能起服务,不需要注册中心、配置中心和网关那套分布式基础设施。拿 Spring Cloud 来做点餐系统,等于面试官问你“订单状态怎么流转”,你先答了三分钟 Nacos 和 OpenFeign,业务本身反而被稀释了。常见做法是 Spring Boot 2.x + MyBatis-Plus + MySQL + Redis 的组合,这套组合在校园项目和中小型内部系统中出现频率最高,资料多,遇到问题也好搜。
前端这边,Vue 2 + Element-UI 仍然是这类项目包的主力搭配。Vue 3 的 Composition API 确实更现代,但很多“高分项目”的原始代码基于 Vue 2 编写,拿到的依赖、路由写法、组件库版本都是配套好的,强行升级 Vue 3 会把this.$router、this.$refs这类 Options API 写法全部推翻,殃及 Element-UI 的兼容性。我的建议是:如果你想在简历上写 Vue 3,可以早起一点,但为了顺利答辩和稳定演示,保持项目原始技术栈,把精力放在业务逻辑和数据库设计上。
2.2 核心表结构:菜品、订单与订单明细为什么必须分离
点餐系统的核心不在“登录注册”,而在订单模型。菜品表很简单,存菜品名、分类、价格、图片、销量和上下架状态;但订单不能把菜品信息直接塞进一个字段里,因为一个订单可能包含多个菜品,每个菜品的数量可能不同,而且菜品价格后续可能调整,订单里必须保留下单那一刻的快照。经典做法是把订单拆成订单主表和订单明细表,主表记录总金额、用户、状态、创建时间,明细表记录每个菜品、数量、下单时单价和快照名称。
先看建表 SQL 的核心部分:
CREATE TABLE `orders` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` VARCHAR(32) NOT NULL COMMENT '订单号,业务唯一', `user_id` BIGINT NOT NULL COMMENT '下单用户ID', `total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2制作中 3待取餐 4已完成 5已取消', `address` VARCHAR(255) DEFAULT NULL COMMENT '配送地址,自提可空', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; CREATE TABLE `order_items` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_id` BIGINT NOT NULL COMMENT '订单主表ID', `dish_id` BIGINT NOT NULL COMMENT '菜品ID', `dish_name` VARCHAR(50) NOT NULL COMMENT '菜品快照名称', `dish_price` DECIMAL(10,2) NOT NULL COMMENT '菜品下单时单价快照', `quantity` INT NOT NULL COMMENT '购买数量', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';order_no用业务唯一键而不是直接拿自增 ID 对外,是为了后续对接支付和查询时有个不可猜测的凭证;order_items里的dish_name和dish_price是快照字段,菜品改名或调价后,历史订单的数据依然真实。这个设计是答辩时非常加分的点,踩过订单系统坑的人都知道,没有快照的订单表是残缺的。建议图纸上把这两张表画成一对多关系,并把“为什么要两张表”这句话写在论文的数据库设计章节里。
2.3 用 SQL 把库初始化出来,并在 application.yml 里接上
拿到源码包后,数据库不是自动生成的,需要手动执行 SQL 脚本。常见做法是将db目录下的初始化脚本导入 Navicat 或命令行。先用 Navicat 新建数据库,字符集选 utf8mb4,排序规则选 utf8mb4_general_ci,然后右键运行 SQL 文件。执行成功后检查表数量,一般包含 user、dish、category、orders、order_items、cart、address 等表,如果缺表请检查是否执行了第二个增量更新脚本。
后端连接配置在src/main/resources/application.yml里,核心配置如下:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/online_order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: yourpassword redis: host: localhost port: 6379 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImplserverTimezone=Asia/Shanghai和GMT+8这两个时间配置必须同时存在,否则插入订单时间会比本地时间差 8 小时,这是最常见的数据库时区坑。log-impl配置成 StdOutImpl 后,控制台会打印每条 SQL,联调时用这个功能看 MyBatis-Plus 实际生成的语句非常方便,上线前再关掉。
3. Spring Boot 后端落地:登录、权限、缓存与订单事务
3.1 JWT 登录与拦截器:放行路径列表是第一个坑
网上点餐系统涉及用户端和管理员端,通常用 JWT 做无状态登录。用户登录成功后,后端返回一个token,前端存在 localStorage 里,每次请求放到 Header 的Authorization字段。拦截器里要判断 token 是否有效,同时必须维护一个“放行路径”列表,否则登录页本身都进不去。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/api/user/login", "/api/user/register", "/api/dish/list", // 菜品浏览不登录 "/api/category/list", // 分类列表不登录 "/static/**", // 静态资源 "/error" ); } }参数说明:/**表示拦截所有请求,excludePathPatterns里的路径按实际接口命名调整。菜品列表和分类列表放行,是因为用户点餐前先看到菜单是基本体验;但加入购物车和提交订单的接口必须拦截。拦截器里解析 token 后,把 userId 放到request.setAttribute("userId", id),Controller 里再从HttpServletRequest拿,不要在拦截器里直接去调数据库查用户,那样每一步请求都多一次 IO。
3.2 菜品分类用 Redis 缓存:缓存穿透与 key 设计
菜品和分类是高频读、低频写的数据,适合放进 Redis。每次用户打开菜单都查一次 MySQL 没问题,但演示时反复开关页面会有大量重复查询,加上缓存后响应速度能快一个量级。常见做法是查询时先读 Redis,没有则查库并回填,缓存过期时间设为 30 分钟。
public List<Dish> listByCategory(Long categoryId) { String key = "dish:category:" + categoryId; String json = redisTemplate.opsForValue().get(key); if (StringUtils.isNotEmpty(json)) { return JSON.parseArray(json, Dish.class); } List<Dish> dishes = this.lambdaQuery() .eq(Dish::getCategoryId, categoryId) .eq(Dish::getStatus, 1) .orderByDesc(Dish::getSales) .list(); if (dishes == null || dishes.isEmpty()) { return dishes; } redisTemplate.opsForValue().set(key, JSON.toJSONString(dishes), 30, TimeUnit.MINUTES); return dishes; }这个写法有三个细节:一是 key 要带业务前缀dish:category:,避免和其他缓存互相覆盖;二是空结果不回填,如果分类下确实没有菜品,每次都查库也能接受,比把空值缓存起来更容易踩空壳问题;三是timeout设 30 分钟,菜品上新或价格变动后最多延迟半小时生效。管理员在后台修改菜品时,必须同步调用redisTemplate.delete(key)清理对应缓存,否则会出现前端改了价格、App 上还是原价的“菜单幽灵”问题。
3.3 下单接口的事务边界与幂等处理
下单是点餐系统最核心的接口,流程包含:校验菜品状态和库存、计算总金额、生成订单主表、生成订单明细、扣减菜品销量或库存、清空用户购物车。整个过程必须在一个事务里,任何一步失败都要回滚,不能让订单明细写了但主表没写。
@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(CreateOrderRequest request, Long userId) { // 1. 校验购物车是否为空 List<Cart> cartList = cartMapper.selectList( new LambdaQueryWrapper<Cart>().eq(Cart::getUserId, userId)); if (cartList == null || cartList.isEmpty()) { throw new BizException("购物车不能为空"); } // 2. 生成订单号:时间戳 + 用户ID后四位 + 随机数 String orderNo = generateOrderNo(userId); // 3. 计算总金额并构建主表 BigDecimal total = calculateTotal(cartList); Orders order = new Orders(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 4. 明细表批量插入 List<OrderItems> items = buildOrderItems(cartList, order.getId()); orderItemMapper.batchInsert(items); // 5. 清空购物车 cartMapper.delete( new LambdaQueryWrapper<Cart>().eq(Cart::getUserId, userId)); return new OrderVO(order); }参数说明:rollbackFor = Exception.class必须写,默认的@Transactional只回滚运行时异常,自定义的BizException如果不继承 RuntimeException,事务不会生效。订单号生成规则里加随机数的原因,是防止同一秒内同一用户下单时产生重复号;把订单号生成逻辑单独抽成方法,后续做幂等校验时直接复用。
订单号的幂等还有一个常见做法是把“用户ID + 时间窗口”作为唯一约束的组成部分。如果你不想在代码里费太多功夫,可以在orders表加一个user_id和create_time的联合索引,配合前端“提交按钮点击后置灰 3 秒”来控制重复点击,这样虽然不能完全防止并发,但对课程设计的演示场景足够。
4. Vue 前端落地:依赖安装、路由传参与请求拦截
4.1 创建 Vue 项目与依赖安装:先把 node_modules 的版本冲突清掉
标题里的 Vue 部分,操作上最容易劝退的不是写页面,而是npm install装依赖那一步。很多“高分项目”用的 Element-UI 版本、vue-router版本和node-sass是绑定在一起的,直接npm install经常报 ERESOLVE 错误。
npm install --legacy-peer-deps这个参数表示绕过 peerDependencies 的严格校验,用旧版的依赖解析逻辑。node-sass是最容易编不过的包,如果你的 Node 版本是 16 以上,通常需要把node-sass换成sass才能顺利安装,安装完成后项目里@import的语法要检查一遍。如果启动时提示Module build failed,先删掉node_modules和package-lock.json,重新npm cache clean --force后再安装,这一步能解决绝大多数依赖损坏问题。
4.2 动态路由与路由参数:页面刷新后 userInfo 为什么丢了
前端项目结构一般有两种做法:把用户端和管理员端放进同一个 Vue 项目,通过路由做区分;或者拆成两个独立工程。常见做法是用一个工程包含两套 Layout,用角色字段跳转不同首页。路由传参最容易踩的坑是:点餐页跳到商品详情页时用了this.$router.push({ path: '/detail', query: { dId: id } }),详情页从this.$route.query.dId取参数,这个链条没问题,但如果你在详情页里刷新浏览器,localStorage里的 token 还在,userInfo却因为存在store(Vuex/Pinia)里而丢失,页面直接 404 或跳回登录页。
解决办法是刷新后重新拉取用户信息。在路由全局守卫里判断,有 token 但 store 里没有 userInfo 时,调用/api/user/info重新获取。
router.beforeEach(async (to, from, next) => { const token = localStorage.getItem('token'); if (token && !store.state.userInfo) { try { const { data } = await getUserInfo(); store.commit('setUserInfo', data); next(); } catch (error) { localStorage.removeItem('token'); next('/login'); } } else { next(); } });同时注意,dishId这类业务参数建议放进query里而不是params,因为query会出现在 URL 上,刷新后依然存在;params在刷新后会丢失,这是“页面刷新白屏”的另一个隐性来源。
4.3 axios 封装:统一处理 token 失效与业务状态码
整个点餐系统的所有接口请求,应该走一个统一的 axios 实例,而不是在每个组件里写axios.get。统一的请求封装里要处理三件事:请求头带上 token、响应里统一判断业务状态码、出现 401 时统一跳转登录页。
import axios from 'axios'; import { ElMessage } from 'element-ui'; import router from '@/router'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); // 请求拦截器 service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器 service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.msg || '系统错误'); return Promise.reject(new Error(res.msg)); } return res; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } ElMessage.error(error.message); return Promise.reject(error); } );参数说明:baseURL设为/api,开发环境由 vite.config 或 vue.config 里的 devServer proxy 把请求转发到后端 8080 端口,可以避开跨域,正式环境则用 Nginx 反向代理。后端返回格式统一为{ code, msg, data }是这套方案的前提,如果源码包里的响应结构是{ success, message, result },对应改拦截器里的字段名即可。
4.4 购物车与菜品列表的双向联动:computed 比 watch 更稳
购物车数量和总价是实时联动的,初学者常写成watch监听购物车数组变化,然后手动更新总价,代码量大且容易遗漏。推荐用 Vue 2 的computed计算属性,直接从购物车 state 派生总价,不需要额外维护数据。
computed: { cartList() { return this.$store.state.cartList; }, totalPrice() { return this.cartList.reduce( (sum, item) => sum + item.price * item.quantity, 0 ).toFixed(2); } }reduce里累加的是每个商品的单价乘数量,toFixed(2)保证价格显示成两位小数。这里要提醒的是不要用watch去改data,否则控制台会出现 “Computed property totalPrice was assigned to but it has no setter” 这类警告。购物车放在 Vuex 里还有一个好处,是刷新页面后虽然 state 丢失,但可以结合localStorage做持久化,在store初始化时读取缓存,让用户刷新后购物车不清空,这个细节能直接写进论文的“系统优化”章节。
5. 订单状态机与并发扣库存:点餐系统最容易暴露的问题点
5.1 订单状态流转:从待支付到已完成,后端要用枚举收口
订单状态如果在前端写死字符串,后端接口直接传字符串改状态,项目跑起来没问题,但答辩时一旦被问到“状态值在哪定义的”,就会暴露设计薄弱。合理的做法是在后端定义一个枚举类把状态收口,接口只接收枚举对应值而不是任意字符串。
public enum OrderStatus { PENDING_PAYMENT(0, "待支付"), PAID(1, "已支付"), PROCESSING(2, "制作中"), READY(3, "待取餐"), COMPLETED(4, "已完成"), CANCELLED(5, "已取消"); private final int value; private final String desc; OrderStatus(int value, String desc) { this.value = value; this.desc = desc; } public int getValue() { return value; } public String getDesc() { return desc; } }状态流转的校验逻辑放在updateStatus方法里:待支付可以取消或支付,已支付才能进入制作中,已完成和已取消是终态,不能往回走。这个校验逻辑写在 service 层,每个改状态的接口都先读订单,再判断当前状态是否允许变更,比在数据库里写触发器更容易读和维护。演示时如果被问“用户端取消订单后,座位或菜品库存怎么处理”,你回答说“取消逻辑里先把订单状态置为已取消,再回补菜品销量”,这就是一个完整的闭环回答。
5.2 并发下单超卖:数据库行锁比 Redis 分布式锁更合适
点餐系统的菜品库存字段通常叫stock或sales。单纯先select再update的方式,在两个人同时下单时会超卖:两个人同时查到库存为 1,各自经过业务判断后都执行扣减,库存变成负数。常见做法有两种,一是 Redis 分布式锁,二是数据库乐观锁或悲观锁。对这个体量的项目,数据库悲观锁足够,而且最好解释。
SELECT stock FROM dish WHERE id = ? FOR UPDATE;FOR UPDATE会锁住这一行,直到事务提交或回滚。第二个请求必须等第一个事务完成后才能读库存,所以不会出现同时读到相同库存的竞态。注意这个查询必须在事务里执行,并且走主键索引才能拿到行锁;如果查询条件不是索引字段,InnoDB 会把锁升级成表锁,拖慢整张菜品表。对应 Java 代码里就是先selectByIdForUpdate,判断库存大于 0 后执行UPDATE dish SET stock = stock - 1 WHERE id = ?。
还有一种更轻的做法是在 update 语句里带条件查库存:
UPDATE dish SET stock = stock - 1 WHERE id = ? AND stock > 0;如果返回的受影响行数为 1,说明扣减成功;为 0 说明库存不足。这个方案不需要额外加锁,代码量更少,缺点是库存充足时并发下没问题,但无法预知实际剩余库存,先扣减后才发现不足就得回滚。推荐的做法是两条路一起走:stock > 0作为兜底条件,前置查询加FOR UPDATE保证业务提示更友好。
5.3 支付回调与本地模拟:用一个 mock 接口闭环联调
真实支付需要商户号、证书等资质,课程设计里往往没有。常见做法是保留前端的“立即支付”按钮,后端接一个 mock 支付接口,按下后把订单状态从待支付改为已支付,再走后面的制作中流程。
@PostMapping("/api/pay/mock") public Result<Void> mockPay(@RequestParam String orderNo, @RequestParam Long userId) { Orders order = orderMapper.selectOne( new LambdaQueryWrapper<Orders>().eq(Orders::getOrderNo, orderNo)); if (order == null || !order.getUserId().equals(userId)) { throw new BizException("订单不存在"); } if (order.getStatus() != OrderStatus.PENDING_PAYMENT.getValue()) { throw new BizException("订单状态不正确"); } order.setStatus(OrderStatus.PAID.getValue()); orderMapper.updateById(order); return Result.success(); }真实支付的回调接口写的是“被动通知”,由支付平台主动请求后端接口,前端轮询订单状态;mock 支付则是前端发起请求后立即改状态。为了让演示更真实,可以加一个“模拟支付延迟”的Thread.sleep(1500)参数,让用户先看到待支付状态再变已支付,演示视频里这个过渡比瞬间变更好看。另外一个加分项是本地模拟“支付回调幂等”:如果前端重复提交两次 mock 请求,第二次要返回“订单状态不正确”而不是把已支付订单再改一遍,这个防御逻辑证明你理解状态机的不可逆性。
5.4 图片上传:本地路径与 OSS 的切换方案
菜品图片上传是这种项目最常见的管理员功能。简单方案是把图片存到本机项目目录下,通过配置静态资源映射来访问,复杂一点是接阿里云 OSS。本地方案虽然不具备生产可用性,但演示时最不容易挂。
spring: web: resources: static-locations: file:D:/upload/,classpath:/static/上传代码将MultipartFile保存到D:/upload目录,文件名用UUID重命名以覆盖同名冲突。注意两点:一是保存路径和访问 URL 要能对应,如果上传到D:/upload/而请求路径是/images/xxx.jpg,要再加一个映射关系或直接用http://localhost:8080/xxx.jpg拼;二是 Windows 和 Linux 路径分隔符不同,建议在配置文件里把upload.path单独抽出来,换环境只改一个配置。对接 OSS 的时机应当放在论文的“系统改进方向”里,既能体现你了解对象存储,又不影响当前演示环境。
6. 跑通后的三件收尾事:环境编排、排错技巧与“能讲出话”的文档
6.1 用 Docker Compose 一键带起 MySQL 与 Redis
本地开发时 MySQL 和 Redis 未安装或版本混乱会导致项目起不来。推荐用 Docker Compose 把两个中间件一次性带起来,环境干净可复现。
version: '3' services: mysql: image: mysql:5.7 container_name: online-order-mysql ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: online_order command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci volumes: - ./mysql-data:/var/lib/mysql - ./db:/docker-entrypoint-initdb.d redis: image: redis:6.2 container_name: online-order-redis ports: - "6379:6379"./db:/docker-entrypoint-initdb.d这个卷挂载会使 MySQL 容器第一次启动时自动执行目录里的.sql脚本,免去手动导入。注意 MySQL 8.0 的认证方式与很多旧驱动不兼容,项目驱动连接不上时先检查容器版本。Redis 容器没有设置密码,application.yml里也不用配密码,保持双端一致。
6.2 常见环境排错:Spring Boot 版本、MAVEN 依赖与 Node 构建
“springboot 版本太高”是绕不开的坑。如果你在 IDEA 里用 Spring Initializr 新建项目,最新版本可能是 Spring Boot 3.x,它用的是jakarta.*命名空间,而课程设计项目基于 Spring Boot 2.x,包名依然是javax.*,直接把源码复制进新项目会编译失败,提示找不到javax.servlet.http.HttpServletRequest。正确做法是先看pom.xml里 parent 的版本,2.x 项目就选 2.7.x 的 Spring Boot,不要顺手选最高版本。
Maven 依赖拉不下来时,先确认settings.xml里用的镜像源,国内环境建议使用阿里云镜像,配置方式是在镜像<mirrorOf>里写central。IDEA 打开项目后要等依赖索引完成再启动,否则会出现ClassNotFoundException,但这只是 IDE 编译缓存问题,敲一遍mvn clean install即可验证。
前端构建环节,npm run build打包后布局异常通常有两种原因:一是打包后的静态资源路径不对,需要检查vue.config.js里的publicPath,改成./相对路径后本地打开 index.html 才能看到页面;二是历史模式刷新 404,这是vue-router的mode: history与静态服务器不匹配导致的,hash模式不会有这个问题,想用 Nginx 部署就配置try_files $uri $uri/ /index.html;,想省事就直接退回 hash 模式。
6.3 给项目做点“证据”:接口清单、数据库文档与演示脚本
答辩和简历里最能证明你做过这个项目的,不是 PPT 里的截图,而是可核查的交付物。第一个是接口清单:把 Spring Boot 里所有 Controller 方法导出成一张表,包含请求方式、路径、入参、出参、是否登录、是否管理员权限。最简单的方式是用 Springfox/knife4j 生成在线文档,启动项目后访问/doc.html就能查看,省去手写时间。第二个是数据库文档:把information_schema里的表结构导出成 Excel,或者用SHOW CREATE TABLE逐个拉出来,至少要准备一页说明各表用途、关键字段含义和表关系,放进论文附录比放进代码更有说服力。第三个是演示脚本:按“管理员上架菜品→用户注册登录→浏览菜单→加购→下单→模拟支付→订单状态流转→后台接单完成”这个顺序把每个操作对应的页面和接口写清楚,演示视频按这个脚本录制,不会出现中途卡顿或忘词的情况。
这三个东西做完,你就着这个压缩包里的源文件,能脱口说出“MySQL 五张核心表、JWT 没状态会话、Redis 缓存菜单分类、悲观锁防超卖、Vue 路由守卫刷新恢复登录态”——顺着这个结构讲,面试官追问任何一层你都还有继续展开的余量。跑通只是起点,把技术点翻译成自己的话才是这一步的终点。
本文还有配套的精品资源,点击获取