微信外卖小程序答辩PPT与Java后端实战:订单状态机、支付回调幂等与库存扣减

发布时间:2026/10/8 20:15:05
微信外卖小程序答辩PPT与Java后端实战:订单状态机、支付回调幂等与库存扣减
简介这份PPT资源面向计算机专业学生与Java开发者用于微信外卖小程序项目的毕业答辩或课程汇报。内容围绕管理员、商家、用户三大服务端展开涵盖食品类型管理、商户与外卖信息管理、订单管理、系统管理等模块并梳理了Java面向对象、跨平台、垃圾回收机制及MySQL数据库、微信开发者工具等关键技术要点可作为答辩演示与项目讲解的参考模板。资源包共1个pptx文件约27.67MB以幻灯片形式呈现系统设计、需求分析、功能实现与测试流程结构清晰便于直接用于汇报展示。目前已有74人学习下载适合需要快速整理答辩思路、了解微信小程序与Java后台结合方案的学生参考也可帮助读者把握外卖类系统的模块划分与开发要点。1. 从一份答辩 PPT 反推微信外卖小程序 Java 后端到底交付了什么答辩 PPT 是整条链路的“黑匣子”入口。评委翻到第 3 页就会问你的微信外卖小程序后端到底跑的是什么订单状态怎么流转支付回调怎么保证不丢单如果 PPT 里只有截图和“基于 Java 开发”六个字这场答辩基本就悬了。我见过太多同学把精力全砸在界面截图上结果老师一句“你这个库存扣减是同步还是异步”直接问穿。这份 PPT 真正要承载的是把 Java 后端、微信小程序前端、MySQL 数据表、订单状态机这四件事讲成一条能自圆其说的线。适合正在做微信小程序毕业设计、课程设计或者准备把外卖类小程序真正跑起来的人。下面按“PPT 怎么搭骨架 → 后端怎么落地 → 数据怎么设计 → 坑在哪”的顺序拆开讲。2. 答辩 PPT 的骨架怎么搭从技术栈到演示动线2.1 先定技术栈再定 PPT 章节很多人的 PPT 是“先写页面再补技术”结果技术章节和演示章节对不上。我的习惯是先把技术栈钉死再让每一页 PPT 都能对应到一条可验证的链路。微信外卖小程序这类项目常见且稳妥的组合是前端用微信小程序原生开发或 uni-app后端用 Spring Boot MyBatis-Plus数据库 MySQL缓存 Redis支付走微信支付。技术栈一旦定了PPT 的章节顺序就自然出来了项目背景 → 需求分析 → 技术选型 → 系统设计 → 核心功能实现 → 测试与演示 → 总结与展望。这里有个容易被忽略的点技术选型页不要只列名词要写“为什么选它”。比如选 MyBatis-Plus是因为外卖场景里订单、菜品、用户三张表的单表 CRUD 极多用 MyBatis-Plus 的BaseMapper能省掉大量重复 SQL选 Redis是因为购物车和菜品缓存读多写少直接压 MySQL 会在答辩演示时卡顿。这些理由写进 PPT老师一看就知道你是真做过。2.2 演示动线要按“用户下单”走一遍答辩演示最忌讳东点一下西点一下。我一般会设计一条固定动线用户登录 → 浏览菜品 → 加入购物车 → 提交订单 → 支付 → 商家接单 → 订单状态变更。这条动线覆盖了微信小程序登录获取手机号、购物车本地存储与后端同步、订单号生成、支付回调、状态机流转这几个核心点。PPT 里每一页对应动线的一个节点演示时按顺序点老师问哪一步你都能接住。具体到 PPT 页面我建议核心功能实现部分至少放三张“有代码”的页一张登录换 openid 的时序一张订单创建的接口入参出参一张支付回调的验签与幂等处理。不要放整屏代码放关键片段加注释即可。比如登录换 openid 那段PPT 上只放wx.login拿到 code、后端调jscode2session、返回自定义登录态这三步旁边标注“code 五分钟有效openid 不暴露给前端”。2.3 用一张状态机图代替十页文字订单状态是外卖小程序答辩被问最多的地方。与其用文字描述“待支付、已支付、待接单、配送中、已完成、已取消”不如在 PPT 里放一张状态流转图标注每个状态由谁触发、超时怎么处理。比如“待支付”超过 15 分钟未支付由定时任务自动取消并回滚库存“已支付”后商家 5 分钟未接单触发提醒。这张图一放老师基本不会再追问状态逻辑因为信息密度够了。提示状态机图不要用花哨的配色黑白加箭头即可重点是箭头上的触发条件和超时时间要写清楚。3. Java 后端落地从实体类到订单接口的最小闭环3.1 用 MyBatis-Plus 根据实体类生成建表 SQL热搜里有人问“mybatisplus 根据 java 实体类生成创建表的 sql 语句”这在外卖项目里很实用因为表多、字段改得勤。MyBatis-Plus 本身不直接生成 DDL但可以借助它的代码生成器或手写一个简单的反射工具。我一般会在项目里放一个TableSqlGenerator读取实体类的TableName和TableField注解拼出CREATE TABLE语句。下面是一个可复现的最小实现import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import java.lang.reflect.Field; public class TableSqlGenerator { public static String generate(Class? clazz) { // 读取类上的表名注解没有则用类名小写 TableName tableName clazz.getAnnotation(TableName.class); String table tableName ! null ? tableName.value() : clazz.getSimpleName().toLowerCase(); StringBuilder sql new StringBuilder(CREATE TABLE table (\n); Field[] fields clazz.getDeclaredFields(); for (int i 0; i fields.length; i) { Field f fields[i]; TableField tf f.getAnnotation(TableField.class); // 跳过非数据库字段 if (tf ! null !tf.exist()) continue; String column tf ! null !tf.value().isEmpty() ? tf.value() : camelToUnderline(f.getName()); sql.append( ).append(column).append( ).append(mapType(f.getType())); if (f.getName().equals(id)) sql.append( PRIMARY KEY AUTO_INCREMENT); sql.append(i fields.length - 1 ? \n : ,\n); } sql.append();); return sql.toString(); } private static String camelToUnderline(String name) { StringBuilder sb new StringBuilder(); for (char c : name.toCharArray()) { if (Character.isUpperCase(c)) sb.append(_).append(Character.toLowerCase(c)); else sb.append(c); } return sb.toString(); } private static String mapType(Class? type) { if (type Long.class || type long.class) return BIGINT; if (type Integer.class || type int.class) return INT; if (type String.class) return VARCHAR(255); if (type java.math.BigDecimal.class) return DECIMAL(10,2); if (type java.util.Date.class) return DATETIME; return VARCHAR(255); } }这段代码的逻辑很直白遍历实体类字段把驼峰转下划线把 Java 类型映射成 MySQL 类型遇到TableField(exist false)就跳过。参数上要注意两点一是TableName不写时默认用类名小写实际项目里最好显式写二是BigDecimal对应金额字段别用Double否则答辩时被问精度会翻车。生成出来的 SQL 只是初稿索引、外键、字符集还得手动补别指望一键生成就能上生产。3.2 订单创建接口入参、事务与库存扣减订单接口是外卖小程序的核心。我一般把入参设计成“用户 ID 商家 ID 菜品列表 收货地址 备注”菜品列表里每项包含菜品 ID 和数量。后端要做的事校验菜品是否上架、计算总价、扣减库存、生成订单号和订单明细、返回订单 ID。这里必须加事务否则扣了库存但订单没生成数据就脏了。Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 1. 校验菜品并计算总价 BigDecimal total BigDecimal.ZERO; for (OrderItemDTO item : dto.getItems()) { Dish dish dishMapper.selectById(item.getDishId()); if (dish null || dish.getStatus() ! 1) { throw new BizException(菜品已下架); } total total.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getCount()))); } // 2. 扣减库存用乐观锁防止超卖 for (OrderItemDTO item : dto.getItems()) { int rows dishMapper.reduceStock(item.getDishId(), item.getCount()); if (rows 0) throw new BizException(库存不足); } // 3. 生成订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setTotalAmount(total); order.setStatus(0); // 0 待支付 orderMapper.insert(order); // 4. 写入订单明细 for (OrderItemDTO item : dto.getItems()) { OrderDetail detail new OrderDetail(); detail.setOrderId(order.getId()); detail.setDishId(item.getDishId()); detail.setCount(item.getCount()); orderDetailMapper.insert(detail); } return new OrderVO(order.getId(), order.getOrderNo(), total); }逻辑说明先校验再扣库存扣库存用UPDATE dish SET stock stock - #{count} WHERE id #{id} AND stock #{count}这种带条件的更新返回影响行数为 0 就说明库存不够直接抛异常回滚。参数上Transactional的rollbackFor要写Exception.class否则遇到受检异常不回滚。订单号生成常见做法是“时间戳 用户 ID 后四位 随机数”别用 UUID太长且无序影响索引。3.3 支付回调的幂等处理微信支付回调是答辩高频追问点。回调可能重复推送所以必须做幂等。我的做法是回调进来先查订单状态如果已经是“已支付”就直接返回成功不再处理否则更新订单状态并记录支付流水。更新时用UPDATE orders SET status 1 WHERE id #{id} AND status 0靠数据库的行锁保证只成功一次。public String handlePayCallback(PayCallbackDTO dto) { // 验签省略实际项目必须验 Order order orderMapper.selectById(dto.getOrderId()); if (order null) return FAIL; // 幂等已支付直接返回 if (order.getStatus() 1) return SUCCESS; int rows orderMapper.updateStatus(order.getId(), 0, 1); if (rows 0) return SUCCESS; // 并发下已被其他线程处理 // 记录支付流水 payLogMapper.insert(new PayLog(order.getId(), dto.getTransactionId())); return SUCCESS; }参数上注意updateStatus的 SQL 要带原状态条件这是幂等的关键。返回给微信的字符串必须是SUCCESS或FAIL别返回 JSON否则微信会一直重推。验签部分在答辩时可以不展开但一定要说“生产环境必须验签”这是态度分。4. 微信小程序端登录、购物车与顶部导航栏适配4.1 微信小程序登录获取手机号的完整链路微信小程序登录获取手机号现在主流做法是wx.login拿 code 换 openid再用getPhoneNumber按钮拿加密数据后端解密。PPT 里要讲清楚code 只能用一次五分钟过期openid 是用户唯一标识但不建议直接暴露给前端手机号解密要用 session_key所以后端得维护登录态。// 小程序端登录并获取手机号 wx.login({ success(res) { if (res.code) { wx.request({ url: https://your-domain/api/login, method: POST, data: { code: res.code }, success(loginRes) { // 拿到自定义登录态 token wx.setStorageSync(token, loginRes.data.token); } }); } } }); // 获取手机号按钮 button open-typegetPhoneNumber bindgetphonenumbergetPhone获取手机号/button getPhone(e) { if (e.detail.code) { wx.request({ url: https://your-domain/api/phone, method: POST, header: { Authorization: wx.getStorageSync(token) }, data: { code: e.detail.code }, success(res) { console.log(手机号, res.data.phone); } }); } }逻辑说明wx.login的 code 传给后端后端调微信接口换 openid 和 session_key生成自定义 token 返回。获取手机号时前端把e.detail.code传给后端后端再用 access_token 去换手机号。参数上注意getPhoneNumber必须由用户点击按钮触发不能自动调用后端换手机号的接口需要 access_token要缓存别每次请求都去换。4.2 购物车本地存储与后端同步购物车在外卖小程序里通常是“本地先存登录后同步”。未登录时用wx.setStorageSync存菜品 ID 和数量登录后调后端接口合并。这里有个坑本地存储的菜品可能已下架同步时要过滤。我一般会在同步接口里返回“失效菜品列表”前端提示用户。// 本地购物车结构 const cart [{ dishId: 1, count: 2 }, { dishId: 3, count: 1 }]; wx.setStorageSync(cart, cart); // 登录后同步 wx.request({ url: https://your-domain/api/cart/sync, method: POST, header: { Authorization: wx.getStorageSync(token) }, data: { items: cart }, success(res) { // res.data.invalid 是失效菜品 if (res.data.invalid.length 0) { wx.showToast({ title: 部分菜品已下架, icon: none }); } wx.setStorageSync(cart, res.data.valid); } });参数上同步接口要做合并逻辑同一菜品数量相加但不超过库存。返回的valid是合并后的购物车前端覆盖本地存储。4.3 顶部导航栏高度适配微信小程序顶部导航栏高度在不同机型上不一样尤其是带刘海屏的。常见做法是用wx.getSystemInfoSync()拿statusBarHeight再结合胶囊按钮位置算导航栏高度。我一般会封装一个getNavBarHeight函数在页面onLoad时设置。function getNavBarHeight() { const sysInfo wx.getSystemInfoSync(); const statusBarHeight sysInfo.statusBarHeight; // 胶囊按钮位置 const menuButton wx.getMenuButtonBoundingClientRect(); // 导航栏高度 胶囊底部到状态栏底部的距离 * 2 胶囊高度 const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height; return statusBarHeight navBarHeight; } Page({ data: { navBarHeight: 0 }, onLoad() { this.setData({ navBarHeight: getNavBarHeight() }); } });逻辑说明statusBarHeight是状态栏高度menuButton.top是胶囊顶部距屏幕顶部的距离两者相减得到胶囊距状态栏的间距乘以 2 加上胶囊高度就是导航栏内容区高度。参数上注意getMenuButtonBoundingClientRect要在页面渲染后调用太早可能拿不到值。这个高度用于自定义导航栏如果直接用默认导航栏则不需要。5. 避坑与排查答辩前必须过的五道坎5.1 订单状态更新了但库存没回滚现象用户取消订单后订单状态变成“已取消”但菜品库存没加回来。原因取消订单的代码只更新了订单表忘了回滚库存或者回滚库存的 SQL 条件写错。解决把“更新订单状态”和“回滚库存”放在同一个事务里回滚库存用UPDATE dish SET stock stock #{count} WHERE id #{id}并在取消前判断订单状态是否为“待支付”或“已支付未接单”。5.2 微信支付回调收不到现象用户支付成功但订单状态还是“待支付”。原因回调地址配置错误或者服务器没有公网 IP或者回调返回的不是SUCCESS。解决检查微信支付商户平台的回调地址是否带https本地开发用内网穿透工具临时映射回调接口返回纯文本SUCCESS不要返回 JSON在回调里打日志确认微信是否真的推过来了。5.3 小程序登录 code 失效现象用户点登录后端报code been used或invalid code。原因code 被用了两次比如前端重复提交或者后端重试机制导致。解决前端登录按钮加防抖后端拿到 code 后先换 openid换失败直接返回错误不要重试同一个 code。另外 code 五分钟过期别缓存。5.4 购物车数量超过库存现象用户把某菜品加到购物车 10 份但库存只有 5 份提交订单时才报错。原因加购时没校验库存或者校验了但没实时更新。解决加购时调后端接口校验库存返回可加数量提交订单时再校验一次双重保险。前端在购物车页面显示“库存不足”提示。5.5 答辩演示时小程序白屏现象本地开发正常答辩现场打开小程序白屏。原因后端服务没启动或者域名没配好或者小程序没开调试模式。解决答辩前把后端服务部署到云服务器配置好合法域名准备一个本地降级方案比如用localhost加“不校验合法域名”选项提前在答辩电脑上登录微信开发者工具别现场扫码。6. 进阶技巧用状态机 定时任务把订单超时取消做扎实订单超时取消是外卖小程序里最能体现工程思维的点。很多人的做法是前端倒计时时间到了调取消接口但用户关掉小程序就不触发了。我的做法是后端用定时任务扫“待支付且创建时间超过 15 分钟”的订单批量取消并回滚库存。这里的关键是定时任务要加分布式锁避免多实例重复执行取消时要幂等防止和用户手动取消冲突。Scheduled(cron 0 */1 * * * ?) // 每分钟执行一次 public void cancelTimeoutOrders() { // 加分布式锁key 为 order:cancel:lock过期时间 55 秒 Boolean lock redisTemplate.opsForValue() .setIfAbsent(order:cancel:lock, 1, 55, TimeUnit.SECONDS); if (Boolean.FALSE.equals(lock)) return; try { // 查 15 分钟前创建的待支付订单 ListOrder orders orderMapper.selectTimeoutOrders(15); for (Order order : orders) { // 幂等取消只有待支付才取消 int rows orderMapper.updateStatus(order.getId(), 0, 5); // 5 已取消 if (rows 0) { // 回滚库存 ListOrderDetail details orderDetailMapper.selectByOrderId(order.getId()); for (OrderDetail d : details) { dishMapper.addStock(d.getDishId(), d.getCount()); } } } } finally { redisTemplate.delete(order:cancel:lock); } }逻辑说明定时任务每分钟跑一次用 Redis 的setIfAbsent做分布式锁锁过期时间比执行间隔短防止死锁。查超时订单时用create_time DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND status 0。取消时用带状态条件的更新保证幂等只有更新成功才回滚库存。参数上注意锁的过期时间要小于定时任务间隔否则下一轮拿不到锁selectTimeoutOrders要加索引否则订单多了会慢查询。验证方法把超时时间改成 1 分钟手动创建一个待支付订单等两分钟看订单是否变成“已取消”且库存回滚。如果没变先看定时任务日志再看 Redis 锁是否释放最后看 SQL 条件是否写错。我自己的习惯是任何涉及状态变更和库存的操作先问自己“重复执行会怎样”如果答案不是“没事”就加幂等。这个习惯帮我省了太多后悔药。希望帮到你。本文还有配套的精品资源点击获取