苍穹外卖-后端模拟微信支付与订单状态流转实战

张开发
2026/4/19 4:25:26 15 分钟阅读

分享文章

苍穹外卖-后端模拟微信支付与订单状态流转实战
1. 苍穹外卖后端模拟支付方案设计在开发外卖系统时支付功能往往是整个业务流程中最关键的环节之一。传统的做法需要对接微信支付官方接口但对于开发测试环境来说这种方式存在几个痛点需要企业资质、涉及真实资金流转、调试流程复杂。我在实际项目中摸索出了一套纯后端模拟方案完全避开了这些麻烦。这个方案的核心思路很简单绕过真实支付接口直接在数据库层面修改订单状态。听起来有点简单粗暴但实测下来发现它不仅完美支持了基础支付功能还能完整覆盖订单状态流转、商家接单、用户催单等全部业务流程。最重要的是这套方案对前端代码零侵入特别适合还在学习阶段的开发者。具体实现上我们需要关注三个关键点支付成功后的订单状态变更待接单、已支付商家端的接单/拒单逻辑WebSocket实时消息推送Day10内容先看支付环节的改造。原生的微信支付接口调用代码大概长这样// 原生微信支付接口调用 JSONObject jsonObject weChatPayUtil.pay( ordersPaymentDTO.getOrderNumber(), new BigDecimal(0.01), 苍穹外卖订单, user.getOpenid() );我们要做的就是把这段代码注释掉替换为直接修改数据库的操作Orders orders orderMapper.getByOrderNum(orderNumber); orders.setStatus(Orders.TO_BE_CONFIRMED); // 设置为待接单状态 orders.setPayStatus(Orders.PAID); // 支付状态设为已支付 orders.setPayMethod(1); // 支付方式为微信支付 orders.setOrderTime(LocalDateTime.now()); // 记录支付时间 orderMapper.update(orders); // 更新数据库这种改造虽然简单但有几个细节需要注意支付时间要准确记录后续超时未接单的逻辑依赖这个时间戳支付状态和订单状态要同步更新避免出现已支付但状态还是未支付的矛盾情况返回的VO对象可以置空因为前端不会真的处理支付结果2. 订单状态流转的完整实现2.1 支付成功后的状态处理支付成功后订单会进入待接单状态TO_BE_CONFIRMED。这个状态的变更需要触发两个关键动作数据库状态的原子性更新通过WebSocket向商家端推送新订单通知在paySuccess方法中我们这样实现public void paySuccess(String outTradeNo) { // 查询订单 Orders ordersDB orderMapper.getByNumber(outTradeNo); // 构建更新对象 Orders orders Orders.builder() .id(ordersDB.getId()) .status(Orders.TO_BE_CONFIRMED) .payStatus(Orders.PAID) .checkoutTime(LocalDateTime.now()) .build(); // 更新数据库 orderMapper.update(orders); // WebSocket推送 MapString, Object map new HashMap(); map.put(type, 1); // 1表示新订单 map.put(orderId, ordersDB.getId()); map.put(content, 订单号 outTradeNo); webSocketServer.sendToAllClient(JSON.toJSONString(map)); }这里有个实战技巧WebSocket消息的type字段非常有用。在我的项目中定义了多种消息类型1新订单通知2催单提醒3订单取消通知4订单完成通知这种设计使得前端可以用同一套WebSocket连接处理各种业务通知。2.2 商家接单与拒单逻辑商家接单相对简单主要是把状态从待接单改为已接单。但拒单逻辑就需要考虑更多边界情况public void reject(OrdersRejectionDTO dto) { // 校验订单是否存在 Orders orderDB orderMapper.getByIdL(dto.getId()); if(orderDB null) { throw new OrderBusinessException(订单不存在); } // 状态校验必须是待接单状态 if(orderDB.getStatus() ! Orders.TO_BE_CONFIRMED) { throw new OrderBusinessException(订单状态异常); } // 支付状态校验必须已支付 if(orderDB.getPayStatus() ! Orders.PAID) { throw new OrderBusinessException(未支付订单不能拒单); } // 更新订单状态 Orders orders Orders.builder() .id(dto.getId()) .rejectionReason(dto.getRejectionReason()) .status(Orders.CANCELLED) .cancelTime(LocalDateTime.now()) .build(); orderMapper.update(orders); }特别注意几点要先查询再更新避免并发问题状态变更前要做充分校验记录拒单原因和操作时间2.3 用户取消订单的特殊处理用户取消订单的逻辑与商家拒单类似但有几点不同取消时间限制比如支付后15分钟内可取消需要区分是用户取消还是系统超时取消可能需要模拟退款流程虽然我们跳过了真实退款public void cancelOrder(Integer id) { Orders orderDB orderMapper.getById(id); // 基础校验 if(orderDB null) { throw new OrderBusinessException(订单不存在); } // 状态校验只能取消待接单和已接单的订单 if(orderDB.getStatus() 2) { throw new OrderBusinessException(订单已开始配送不能取消); } // 构建更新对象 Orders orders new Orders(); orders.setId(orderDB.getId()); orders.setStatus(Orders.CANCELLED); orders.setCancelReason(用户取消); orders.setCancelTime(LocalDateTime.now()); // 如果是已支付订单需要标记为已退款 if(orderDB.getPayStatus() Orders.PAID) { orders.setPayStatus(Orders.REFUND); } orderMapper.update(orders); }3. WebSocket实时通知的实现3.1 WebSocket服务端配置Day10的WebSocket实现是整个系统的亮点。先看服务端的基础配置ServerEndpoint(/ws/{sid}) Component public class WebSocketServer { // 静态变量记录所有连接 private static ConcurrentHashMapString, WebSocketServer webSocketMap new ConcurrentHashMap(); // 与某个客户端的连接会话 private Session session; // 连接建立成功调用的方法 OnOpen public void onOpen(Session session, PathParam(sid) String sid) { this.session session; webSocketMap.put(sid, this); } // 收到客户端消息后调用的方法 OnMessage public void onMessage(String message, Session session) { // 处理客户端消息 } // 向所有客户端推送消息 public static void sendToAllClient(String message) { for (WebSocketServer item : webSocketMap.values()) { try { item.session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } } }关键点使用ServerEndpoint注解声明WebSocket端点静态ConcurrentHashMap保存所有活跃连接提供向所有客户端广播消息的方法3.2 业务消息的推送时机在我们的模拟方案中有四个关键节点需要推送WebSocket消息支付成功时通知商家有新订单// 在paySuccess方法中 MapString, Object map new HashMap(); map.put(type, 1); // 新订单 map.put(orderId, orderId); map.put(content, 新订单 orderNumber); webSocketServer.sendToAllClient(JSON.toJSONString(map));用户催单时MapString, Object map new HashMap(); map.put(type, 2); // 催单 map.put(orderId, orderId); map.put(content, 订单催单 orderNumber); webSocketServer.sendToAllClient(JSON.toJSONString(map));订单状态变更时MapString, Object map new HashMap(); map.put(type, 3); // 状态变更 map.put(orderId, orderId); map.put(content, 订单状态更新 newStatus); webSocketServer.sendToAllClient(JSON.toJSONString(map));订单完成时MapString, Object map new HashMap(); map.put(type, 4); // 订单完成 map.put(orderId, orderId); map.put(content, 订单已完成 orderNumber); webSocketServer.sendToAllClient(JSON.toJSONString(map));3.3 前端消息处理示例虽然我们主要关注后端实现但了解前端如何处理这些消息也很重要。前端JavaScript代码大概长这样const socket new WebSocket(ws://your-domain/ws/123); socket.onmessage function(event) { const data JSON.parse(event.data); switch(data.type) { case 1: // 新订单 showNewOrderAlert(data.content); break; case 2: // 催单 playUrgentSound(data.content); break; case 3: // 状态变更 updateOrderStatus(data.orderId, data.content); break; case 4: // 订单完成 showCompleteNotification(data.content); break; } };4. 测试与调试技巧4.1 支付功能测试测试模拟支付功能时我推荐使用Postman直接调用支付接口。请求示例{ orderNumber: 20230801123456, payMethod: 1 }预期结果数据库orders表对应记录的状态变为2待接单pay_status变为1已支付pay_time字段被更新为当前时间商家端收到WebSocket新订单通知4.2 接单/拒单测试测试接单功能时需要先确保订单处于待接单状态。测试用例设计建议正常接单流程重复接单测试应该失败接已取消订单测试应该失败接不存在的订单测试应该失败对于拒单功能额外测试不带拒单原因的拒单应该失败超长拒单原因测试检查数据库字段长度限制4.3 WebSocket消息测试测试WebSocket时可以使用Chrome的Simple WebSocket Client插件。连接后观察以下场景支付成功后是否收到type1的消息用户催单后是否收到type2的消息订单状态变更时是否收到type3的消息4.4 常见问题排查在实际项目中我遇到过几个典型问题WebSocket连接不稳定解决方案是增加心跳机制每30秒发送一次ping/pong订单状态不一致原因是部分代码直接更新了status但忘记更新pay_status。解决方法是用Builder模式确保所有相关字段一起更新并发修改问题多个操作同时修改同一订单。解决方法是在更新前先查询当前状态或者使用乐观锁5. 方案优化与扩展5.1 状态机模式的应用随着业务复杂度的增加简单的if-else状态判断会变得难以维护。我后来重构使用了状态机模式public class OrderStateMachine { private OrderState currentState; public void toNextState(OrderEvent event) { currentState.handle(this, event); } // 状态定义 public interface OrderState { void handle(OrderStateMachine machine, OrderEvent event); } // 事件定义 public enum OrderEvent { PAY_SUCCESS, MERCHANT_CONFIRM, MERCHANT_REJECT, USER_CANCEL, DELIVERY_START, DELIVERY_COMPLETE } }这种设计使得状态流转逻辑更加清晰也更容易扩展新状态。5.2 分布式环境下的考虑如果系统部署在多台服务器上WebSocket广播就需要特殊处理。我采用的方案是使用Redis的Pub/Sub功能作为消息总线每台服务器订阅特定频道收到消息后在本机WebSocket连接上广播关键代码Configuration public class RedisConfig { Bean public RedisMessageListenerContainer container(RedisConnectionFactory factory, MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(factory); container.addMessageListener(listenerAdapter, new PatternTopic(order_notification)); return container; } } Component public class RedisMessageReceiver { public void handleMessage(String message) { WebSocketServer.sendToAllClient(message); } }5.3 与真实支付接口的平滑切换虽然本文介绍的是模拟方案但实际项目中最终还是要对接真实支付。我建议在代码中保留切换开关Value(${payment.mock.enabled:true}) private boolean mockEnabled; public OrderPaymentVO payment(OrdersPaymentDTO dto) { if (mockEnabled) { // 模拟支付逻辑 } else { // 真实微信支付逻辑 } }这样可以在配置文件中随时切换模拟和真实环境而不用修改代码。

更多文章