简介:面向2025届毕业设计及课程设计场景的零食商城系统完整项目,基于SpringBoot3与Vue.js3实现前后端分离,以MySQL8为数据存储,适合正在开发商城类系统的学生参照学习。资源包共6个文件,压缩包总大小约105.99MB,包含源码zip、数据库sql、需求文档txt、演示录屏mp4等类型,其中源码与数据库脚本可直接用于项目运行,文档辅助理解业务需求,录屏用于查看最终效果。目前已有117人学习下载,项目配套B站启动教程,从环境配置到前后端联调均有直观指引,能有效降低上手门槛。通过该资源可掌握电商后台商品管理、订单处理以及用户前台浏览购买交互的实现逻辑,学习SpringBoot3接口开发与Vue3组件化前端的整合方式,并可直接部署作为毕设演示或二次开发基础。
1. 用 SpringBoot3 + Vue.js3 做零食商城,先想清楚这四件事
零食商城是毕业设计里的高频题目,业务边界清楚:商品浏览、购物车、下单、订单管理,每块都能单独展开。选 SpringBoot3 + Vue.js3 不是因为版本新,而是这套前后端分离写法覆盖了当前 Java 和前端岗位最常见的技能栈。SpringBoot3 要求 JDK 17,Vue.js3 组合式 API 搭配 Vite 也已取代 Vue 2 的主流写法,答辩时讲版本迁移理由是加分项。
动手前先定四件事:商城只做用户购买链路,还是连后台管理一起做;文档用 Knife4j 自动生成还是手工维护;通知用页面轮询还是 MQTT 推送;支付用模拟支付还是沙箱。我建议前三件做扎实,支付用模拟支付带过。这个题目适合有 Java 和 JavaScript 基础、想完整走一遍前后端分离项目的人,下文按后端、前端、交易链路、联调部署四步展开,给可直接落地的代码。
2. 后端工程:IDEA 创建 SpringBoot3 项目与商品接口落地
2.1 SpringBoot3 的版本与 JDK 选型,别被 2.x 的惯性带偏
SpringBoot3 和 2.x 最大的差别不在注解,而在底层约束。3.x 把命名空间从javax.*整体迁移到jakarta.*,同时基线要求 JDK 17。这导致网上大量 2.7 时代的代码复制过来直接报ClassNotFoundException: javax.servlet.Filter。如果你的机器还装着 JDK 8,先装 JDK 17,并确认 IDEA 的 Project Structure 和 Maven 的 Java compiler 都指向 17,再开始建项目。
版本上我一般选 3.2 或 3.3 的正式发布版,配 Maven 使用。Maven 比 Gradle 在毕业设计里更常见,依赖冲突少,答辩也好解释。Spring Initializr 里必须勾选的依赖是 Spring Web、Validation、MySQL Driver、Lombok;数据访问层如果直接写 SQL,就加 mybatis-spring-boot-starter,如果不想写 SQL 就换 Spring Data JPA。下面按 MyBatis 路线展开,因为商品、订单的列表查询写 SQL 更直观,也方便展示索引和锁的用法。
2.2 用 IDEA 创建一个 SpringBoot3 项目的最小步骤
新建项目的操作路径:IDEA 里 File → New → Project,左侧选 Spring Initializr,SDK 选 17,Group 填com.example,Artifact 填snack-mall,然后勾选 Spring Web、Validation、MySQL Driver、Lombok,Generate 之后等 Maven 把依赖拉完。pom.xml 里最关键的内容是 parent 版本和数据访问依赖:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>这里容易出现两个坑:第一,parent 版本还停留在 2.7,导致jakarta包全部飘红,解决办法是把版本改成 3.x 并刷新 Maven;第二,mysql 驱动在 SpringBoot3 里是com.mysql:mysql-connector-j,不再是旧的mysql:mysql-connector-java,直接复制旧依赖会下载失败。
依赖就位后,把src/main/resources/application.yml写成下面这样:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/snack_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.snackmall.entityserverTimezone=Asia/Shanghai是为了避免 MySQL 8 的时区报错;characterEncoding=utf8防止中文乱码;mapper-locations指向 XML 文件目录,type-aliases-package让我们在 XML 里写返回值类型时可以直接写类名,不用带全限定名。IDE 创建 SpringBoot3 项目后建议按这张表检查一遍再写业务:
| 检查项 | 要求 | 常见失败现象 |
|---|---|---|
| JDK | 17 及以上 | 编译报 invalid source release |
| Maven | 3.6.3 及以上 | 下载依赖超时或解析失败 |
| Spring Boot Parent | 3.x | 启动时找不到 jakarta 包 |
| MySQL | 8.0 及以上 | 驱动类找不到或连接被拒绝 |
2.3 分层落盘:controller / service / mapper 的边界怎么定
后端按 controller → service → mapper 三层切,controller 只做参数接收和响应包装,service 管业务规则和事务,mapper 管 SQL。这个分层每层职责单一,答辩时能讲清楚为什么这么分,也能在出问题的时候快速定位。项目的包结构如下:
com.example.snackmall ├── controller # 商品、订单、用户接口 ├── service # 业务逻辑与事务 ├── mapper # MyBatis 接口 ├── entity # 与表对应的实体 ├── config # Knife4j、MQTT 配置 └── common # Result 包装类、业务异常商品实体直接用 Lombok 的@Data减少样板代码:
@Data public class Product { private Long id; private String name; private String category; private BigDecimal price; private Integer stock; private String image; private Integer status; // 1 上架,0 下架 }价格必须用BigDecimal而不是double,零食单价的小数位在计算总价时用浮点会出精度问题,答辩时被问到这一点能答上来是加分项。对应的 controller 如下:
@RestController @RequestMapping("/api/product") @RequiredArgsConstructor public class ProductController { private final ProductService productService; @GetMapping("/list") public Result<PageResult<Product>> list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "12") Integer size) { return Result.ok(productService.page(page, size)); } @GetMapping("/{id}") public Result<Product> detail(@PathVariable Long id) { return Result.ok(productService.getById(id)); } }@RequiredArgsConstructor生成构造器注入,比字段上直接@Autowired更利于测试和循环依赖排查;Result<T>是统一响应包装,前端 axios 拦截器只认这个结构,后文会用到。注意接口前缀统一带/api,和前端请求路径保持一致,后面做请求转发时就不用改路径。
2.4 SpringBoot3 使用 Knife4j 生成接口文档,比 Swagger UI 省事
接口多了之后手工维护文档不现实,常见做法是接 Knife4j。它在 SpringBoot3 下的依赖和 2.x 不同,必须用knife4j-openapi3-jakarta-spring-boot-starter,群坐标com.github.xiaoymin,别再用老版本的knife4j-spring-boot-starter,那个在 3.x 工程里跑不起来:
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <version>4.5.0</version> </dependency>加一个 OpenAPI 的配置类,让文档有标题和版本信息:
@Configuration public class Knife4jConfig { @Bean public OpenAPI openAPI() { return new OpenAPI() .info(new Info().title("零食商城接口文档") .version("1.0") .description("SpringBoot3 + Vue.js3 前后端分离项目")); } }启动后访问http://localhost:8080/doc.html就能看到接口列表。为了让文档可读,controller 类上加@Tag(name = "商品接口"),方法上加@Operation(summary = "分页查询商品"),实体字段用@Schema(description = "商品名称")描述。常用注解对照如下:
| 注解 | 使用位置 | 作用 |
|---|---|---|
@Tag | controller 类 | 接口分组标题 |
@Operation | 接口方法 | 接口名称与描述 |
@Parameter | 方法参数 | 参数说明 |
@Schema | 实体字段 | 字段说明 |
提示:Knife4j 生成的文档默认按 controller 路径分组,如果你发现前端请求的
/api/product/list在文档里点不开,检查@RequestMapping前缀是否带/api,两处必须一致。
3. 前端工程:Vue.js3 + Vite 搭建商城页面与请求闭环
3.1 Vite 创建 Vue.js3 项目的命令与目录调整
前端用 Vite 脚手架初始化,Vue.js3 的组合式 API 在这个模板里默认开启,不需要额外配置。执行下面的命令:
npm create vite@latest snack-mall-web -- --template vue cd snack-mall-web npm install npm install axios pinia element-plus npm run dev依赖里 axios 负责请求,pinia 替代 Vuex 做状态管理,element-plus 是组件库。启动后默认端口是 5173,后端是 8080,跨端口访问后端接口会触发 CORS。开发环境下最省事的办法是在 Vite 里做请求转发,把/api开头的请求转到后端,配置如下:
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })changeOrigin: true让后端看到的请求来源变成后端地址,避免来源校验失败。前后端联调时我一般不开后端的 CORS 配置,而是在开发环境用这个转发、生产环境用 Nginx 转发,一套路径两种环境下都能跑。前端目录按职责组织,答辩时按这个结构讲比按组件讲更清晰:
| 目录 | 职责 | 主要文件 |
|---|---|---|
src/api | 按模块封装请求 | product.js、order.js、user.js |
src/stores | Pinia 状态 | cart.js、user.js |
src/views | 页面 | Home.vue、Cart.vue、Checkout.vue、Login.vue |
src/router | 路由与守卫 | index.js |
src/components | 复用组件 | ProductCard.vue、QuantityInput.vue |
3.2 axios 封装与 Pinia 管理用户态
所有请求统一走一个封装好的 axios 实例,好处是 token 注入、错误提示、登录态失效处理只写一遍。src/api/request.js如下:
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 8000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( res => res.data, err => { if (err.response?.status === 401) { localStorage.removeItem('token') location.href = '/login' } else { ElMessage.error(err.response?.data?.message || '请求失败') } return Promise.reject(err) } ) export default requestbaseURL 写/api,配合 3.1 的转发,开发环境请求/api/product/list实际打到http://localhost:8080/api/product/list,前后端接口前缀约定一次,后续不用再改。响应拦截器直接返回res.data,因为后端统一包在Result<T>里,页面拿到的就是业务数据,不用每处都剥一层。
登录态用 Pinia 管理,同时把 token 落到 localStorage,刷新页面不丢:
// src/stores/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: {} }), getters: { isLogin: state => !!state.token }, actions: { setToken(token) { this.token = token localStorage.setItem('token', token) }, logout() { this.token = '' this.userInfo = {} localStorage.removeItem('token') } } })有的同学喜欢把 token 放 sessionStorage,装完系统关浏览器再开要重新登录,答辩演示时来回切换很尴尬,我建议用 localStorage。getter 里用!!state.token转成布尔值,模板里判断登录态时写起来干净。
3.3 商品列表、购物车、下单页的 Vue.js3 组合式写法
Vue.js3 的<script setup>写法下,页面逻辑按功能组织,商品列表页挂在onMounted里拉数据,loading 状态用ref控制:
<script setup> import { ref, onMounted } from 'vue' import { getProducts } from '@/api/product' const products = ref([]) const loading = ref(false) onMounted(async () => { loading.value = true try { const res = await getProducts({ page: 1, size: 12 }) products.value = res.data.records } finally { loading.value = false } }) </script> <template> <div v-loading="loading" class="product-grid"> <ProductCard v-for="p in products" :key="p.id" :product="p" @add-cart="cartStore.add(p)" /> </div> </template>组合式 API 里ref包装基本类型和数组,模板中自动解包,脚本里要写.value;onMounted代替 Vue 2 的created/mounted。这里一个小技巧:接口返回的res在拦截器里已经被剥成Result结构,所以页面拿res.data.records就是分页记录,不会多一层。
购物车状态放 Pinia,加购、改数量、算总价都集中在 store 里,页面之间共享同一份数据:
// src/stores/cart.js export const useCartStore = defineStore('cart', { state: () => ({ items: [] }), getters: { totalCount: state => state.items.reduce((sum, i) => sum + i.count, 0), totalPrice: state => state.items.reduce((sum, i) => sum + i.price * i.count, 0) }, actions: { add(product) { const exist = this.items.find(i => i.id === product.id) if (exist) { exist.count++ } else { this.items.push({ ...product, count: 1 }) } }, remove(id) { this.items = this.items.filter(i => i.id !== id) } } })getter 里用reduce累加,返回的是计算属性,模板里直接cartStore.totalPrice就拿到总价,不需要手动刷新。注意加购时用{ ...product, count: 1 }展开对象,避免 store 里的 items 和商品列表对象共享引用,否则在页面 A 改了数量,页面 B 的商品数据也跟着变,这种隐式耦合在联调时很难排查。
下单页只需要把购物车数据组装成订单参数提交:
const submitOrder = async () => { const order = await createOrder({ items: cartStore.items.map(i => ({ productId: i.id, num: i.count })) }) router.push(`/order/pay/${order.data.orderNo}`) }下单接口的返回要带回orderNo,支付页靠它去模拟支付,这个字段在下一章后端事务里生成,前后端字段约定在这里闭合。
4. 下单链路的设计:库存扣减、事务边界与 MQTT 通知
4.1 下单接口的事务边界:哪些操作必须在一个事务里
一个创建订单的接口里其实混着四件事:校验库存、写订单、扣库存、发通知。前三个必须在一个事务里,否则会出现订单写了但库存没扣、或者扣了库存订单失败的脏数据。通知则应该在事务提交之后再发,因为事务回滚时消息已经发出去了,用户会收到一条假的成功通知。
下面是订单服务里创建订单的核心逻辑:
@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long userId, Long productId, Integer num) { Product product = productMapper.selectByPessimisticLock(productId); if (product.getStock() < num) { throw new BizException("库存不足"); } String orderNo = "SN" + System.currentTimeMillis(); Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setProductId(productId); order.setNum(num); order.setAmount(product.getPrice().multiply(BigDecimal.valueOf(num))); order.setStatus(0); // 0 待支付 orderMapper.insert(order); int rows = productMapper.deductStock(productId, num); if (rows == 0) { throw new BizException("库存不足,订单已回滚"); } TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { mqttGateway.sendToTopic( "order/notify/" + userId, "您的订单 " + orderNo + " 已创建,请尽快支付"); } }); return OrderVO.from(order); }@Transactional(rollbackFor = Exception.class)指定任何异常都回滚,而不是只回滚 RuntimeException,这样BizException这类自定义业务异常也能触发回滚。注意金额计算用multiply而不是*,BigDecimal 的运算方法调用顺序写错会拿不到正确结果。
事务提交后的通知用TransactionSynchronizationManager.registerSynchronization的afterCommit回调,这是 Spring 官方支持的写法,事务成功提交才执行回调,回滚则不执行。如果直接把 MQTT 发送写在方法体里,事务回滚时会发出假通知,联调时容易误判系统到底有没有成功。
4.2 防超卖的落地组合:乐观锁 SQL 加订单号唯一索引
下单接口在高并发下最容易出问题的是库存扣成负数。常见的错误做法是先查库存、再判断、再 update,这三步之间有间隙,两个请求同时读到库存为 1 就都可能通过校验。正确的做法是让扣库存的 SQL 自己带条件,把判断和更新合成一条原子操作:
<!-- ProductMapper.xml --> <update id="deductStock"> UPDATE product SET stock = stock - #{num} WHERE id = #{id} AND stock >= #{num} </update>这条 SQL 的含义是:只有当前库存大于等于购买数量才执行扣减,stock >= #{num}条件不满足时影响行数为 0,代码里通过rows == 0判断并抛出异常。MyBatis 的 update 返回受影响行数,天然适合做这种乐观锁判断,不用额外引入 version 字段,实现最简。
订单表再加一个唯一索引兜底,防止用户重复点击下单按钮产生重复订单:
CREATE TABLE `t_order` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL, `user_id` BIGINT NOT NULL, `amount` DECIMAL(10,2) NOT NULL, `status` TINYINT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;order_no加唯一索引之后,即使前端按钮没做防重复提交、后端又并发收到两个相同订单号,数据库也会拒绝第二个插入,抛 DuplicateKeyException。在 service 里可以捕获这个异常并返回“请勿重复提交”,比单纯依赖前端禁用按钮可靠得多。
库存扣减和订单号唯一索引配合使用时,我建议把应用层事务的回滚条件设得宽一点:任何一步失败就整体回滚,宁可让用户重新下单,也不能出现半截数据。答辩时面试官如果问“超卖了怎么办”,就按这两条回答:SQL 条件更新兜底并发,唯一索引兜底重复请求,MySQL 的 InnoDB 行锁保证条件更新是原子的。
4.3 SpringBoot3 集成 MQTT,做订单状态通知
页面轮询订单状态虽然简单,但消息实时性差,而且轮询接口会频繁打后端。SpringBoot3 集成 MQTT 是更轻量的方案,MQTT 基于发布订阅模型,用户下单后后端把消息发布到order/notify/{userId}主题,前端订阅这个主题就能实时收到通知。本地开发用 Mosquitto 或 EMQX 起一个 broker,默认端口 1883。
首先引入 Spring Integration 的 MQTT 适配器,这是 Spring 官方对 MQTT 的集成方案,比直接用 Paho 客户端写回调省事:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId> </dependency>配置类里创建连接工厂和收发两条链路:
@Configuration public class MqttConfig { @Value("${mqtt.host}") private String host; @Value("${mqtt.username}") private String username; @Value("${mqtt.password}") private String password; @Value("${mqtt.client-id}") private String clientId; @Value("${mqtt.topic}") private String topic; @Bean public MqttPahoClientFactory mqttClientFactory() { DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); MqttConnectOptions options = new MqttConnectOptions(); options.setServerURIs(new String[]{host}); options.setUserName(username); options.setPassword(password.toCharArray()); options.setAutomaticReconnect(true); factory.setConnectionOptions(options); return factory; } @Bean public MessageProducer mqttInbound() { MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter( clientId, mqttClientFactory(), topic); adapter.setCompletionTimeout(3000); adapter.setQos(1); adapter.setOutputChannel(mqttInputChannel()); return adapter; } @Bean public MessageChannel mqttInputChannel() { return new DirectChannel(); } @Bean public IntegrationFlow mqttInboundFlow() { return IntegrationFlow.from(mqttInputChannel()) .handle(message -> { String payload = new String((byte[]) message.getPayload()); String receivedTopic = (String) message.getHeaders().get("mqtt_receivedTopic"); System.out.println(receivedTopic + " => " + payload); // 这里把消息写入 t_notify 表或通过 WebSocket 推给前端 }) .get(); } @Bean @ServiceActivator(inputChannel = "mqttOutboundChannel") public MessageHandler mqttOutbound() { MqttPahoMessageHandler handler = new MqttPahoMessageHandler(clientId + "-out", mqttClientFactory()); handler.setAsync(true); handler.setDefaultTopic("order/notify"); return handler; } @MessagingGateway(defaultRequestChannel = "mqttOutboundChannel") public interface MqttGateway { void sendToTopic(String topic, String payload); } }配置里几个参数含义要能说清楚,答辩常问:
| 参数 | 含义 | 设置建议 |
|---|---|---|
setAutomaticReconnect(true) | 断线自动重连 | 必须开,broker 重启后能自动恢复 |
setQos(1) | 消息至少投递一次 | 订单通知用 QoS 1,允许重复但不容丢失 |
setAsync(true) | 发送不阻塞主线程 | 下单接口响应更快 |
setDefaultTopic | 未指定主题时的默认主题 | 与订阅主题保持一致 |
订阅主题用order/notify/+,+是 MQTT 的单层通配符,能匹配所有用户的主题。4.1 里mqttGateway.sendToTopic("order/notify/" + userId, ...)发布消息后,这条 inbound 链路会收到,生命周期从订单服务发布到 handler 落库,整条链路闭环。前端用 mqtt.js 订阅同一个 broker,收到消息后弹出 Element Plus 的 Notification,就实现了实时订单通知,不用页面轮询。
注意:clientId 在同一个 broker 上必须唯一,如果后端连了多个实例,每个实例的 clientId 要加随机后缀,否则后连接的实例会把先连接的踢下线,表现为消息时有时无。
5. 上线前联调:构建部署、排错对照表与答辩演示顺序
5.1 前端构建与 Nginx 转发
前端开发完成后的构建命令只有一条:
npm run build产物在dist/目录。把这个目录拷到服务器,用 Nginx 托管,同时把/api开头的请求转发到后端,省去跨域配置:
server { listen 80; server_name localhost; root /usr/share/nginx/html/snack-mall; index index.html; location /api/ { proxy_pass http://localhost:8080/api/; } location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html这一行不能省,Vue Router 用 history 模式时,刷新/product/1这样的地址会先经过 Nginx,如果找不到对应文件就回退到 index.html,由前端路由接管。少了这行,刷新页面会 404。
5.2 高频报错排查对照表
前后端联调阶段的问题集中在环境、路径和依赖三块,按表排查最快:
| 症状 | 原因 | 处理 |
|---|---|---|
启动报ClassNotFoundException: javax.servlet.* | SpringBoot3 下还引了 2.x 依赖 | 统一升级到 3.x 版本,确认 parent 版本 |
访问/doc.html404 | Knife4j 引入的是旧版 starter | 换用knife4j-openapi3-jakarta-spring-boot-starter4.x |
| 前端报 CORS 或跨域 | 请求没走转发,直接访问后端端口 | dev 环境配 Vite proxy,生产环境用 Nginx 转发 |
| 下单后库存变成负数 | 扣库存 SQL 没带stock >= num条件 | 改成 4.2 的条件更新写法 |
| MQTT 收不到消息 | clientId 冲突或订阅主题不匹配 | 换唯一 clientId,核对+通配符 |
| 查询结果中文乱码 | JDBC URL 缺编码参数 | 连接串加characterEncoding=utf8 |
5.3 答辩演示前,先把接口验证跑一遍
演示前用 curl 把核心接口过一遍,比在页面上点更可控,也方便临时查错:
# 登录拿 token curl -X POST http://localhost:8080/api/user/login \ -H "Content-Type: application/json" \ -d '{"username":"student","password":"123456"}' # 查商品列表 curl http://localhost:8080/api/product/list?page=1&size=10 # 下单,Authorization 填上一步返回的 token curl -X POST http://localhost:8080/api/order/create \ -H "Authorization: Bearer <token>" \ -d '{"productId":1,"num":2}'模拟支付接口按orderNo把订单状态从 0 改成 1,调用一次就能在订单列表看到状态流转。我一般会在答辩现场把 MQTT 的订阅端挂在屏幕上,用客户端订阅order/notify/#,页面上下单的同时看到实时消息弹出来,这个演示效果比任何截图都直观。演示顺序固定为:注册登录、浏览商品、加购、下单、模拟支付、订单状态变化,六个步骤对应六个接口,每一个都能在 Knife4j 文档里对应到具体接口定义,问到哪一层都能翻回去看代码。
本文还有配套的精品资源,点击获取