订单核心模块下单流程、状态机、防重下单设计如果说购物车是开门快乐拿那订单就是关门老实付。订单模块承载着从结算到支付的完整链路也是整个系统中并发压力最大、数据一致性要求最高的模块。一、订单在无人售货柜中的核心地位整条业务链回顾一下扫码开门 → 拿商品 → 关门 → 生成订单 → 支付 → 完成 ↑ 订单中心订单是交易的载体它的职责包括记录谁在哪个柜子买了什么驱动库存真实扣减触发支付流程作为售后凭证退款、申诉、对账二、订单状态机设计一个健壮的状态机是订单模块的灵魂┌──────────────┐ │ PENDING │ ← 关门后等待设备上报最终清单 └──────┬───────┘ │ 设备上报完成 ▼ ┌──────────────┐ ┌────────│ UNPAID │ ← 已生成待支付 │ └──────┬───────┘ │ │ 用户发起支付 │ ▼ │ ┌──────────────┐ │ ┌────│ PAID │────┐ │ │ └──────────────┘ │ │ │ 超时/手动取消 │ 支付成功 │ ▼ ▼ │ ┌──────────┐ ┌──────────────┐ │ │ CANCELLED │ │ COMPLETED │ │ └──────────┘ └──────┬───────┘ │ │ 用户申请退款 │ ▼ │ ┌──────────────┐ └─────────────────────│ REFUNDED │ └──────────────┘6个状态详解状态含义触发事件允许的操作PENDING待结算关门事件等待设备清单UNPAID待支付设备清单确认发起支付 / 取消PAID已支付支付成功完成 / 退款COMPLETED已完成系统确认仅查看CANCELLED已取消超时/手动取消仅查看REFUNDED已退款退款成功仅查看publicenumOrderStatus{PENDING(0,待结算),UNPAID(1,待支付),PAID(2,已支付),COMPLETED(3,已完成),CANCELLED(4,已取消),REFUNDED(5,已退款);// 允许的状态流转白名单static{transitionsMap.of(PENDING,Set.of(UNPAID,CANCELLED),UNPAID,Set.of(PAID,CANCELLED),PAID,Set.of(COMPLETED,REFUNDED,CANCELLED),COMPLETED,Set.of(REFUNDED),CANCELLED,Set.of(),REFUNDED,Set.of());}}三、订单表结构CREATETABLEorder_info(idbigintNOTNULLCOMMENT订单ID(雪花算法),order_novarchar(32)NOTNULLCOMMENT订单号(展示用),user_idbigintNOTNULLCOMMENT用户ID,device_idbigintNOTNULLCOMMENT设备ID,cart_idbigintNOTNULLCOMMENT关联购物车ID,command_idvarchar(64)NOTNULLCOMMENT设备指令ID(防重),statustinyintNOTNULLDEFAULT0COMMENT状态0-待结算 1-待支付 2-已支付 3-已完成 4-已取消 5-已退款,total_amountdecimal(10,2)NOTNULLCOMMENT订单总金额,discount_amountdecimal(10,2)DEFAULT0.00COMMENT优惠金额,pay_amountdecimal(10,2)NOTNULLCOMMENT实付金额,pay_timedatetimeDEFAULTNULLCOMMENT支付时间,pay_channelvarchar(20)DEFAULTNULLCOMMENT支付渠道WECHAT/ALIPAY,transaction_idvarchar(64)DEFAULTNULLCOMMENT第三方交易流水号,cancel_timedatetimeDEFAULTNULLCOMMENT取消时间,cancel_reasonvarchar(100)DEFAULTNULLCOMMENT取消原因,expire_timedatetimeNOTNULLCOMMENT支付超时时间(创建后30分钟),create_timedatetimeNOTNULLDEFAULTCURRENT_TIMESTAMP,update_timedatetimeNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(id),UNIQUEKEYuk_order_no(order_no),UNIQUEKEYuk_command_id(command_id),UNIQUEKEYuk_cart_id(cart_id),KEYidx_user_id_create_time(user_id,create_time),KEYidx_status_expire(status,expire_time))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT订单表;CREATETABLEorder_item(idbigintNOTNULL,order_idbigintNOTNULLCOMMENT订单ID,product_idbigintNOTNULLCOMMENT商品ID,product_namevarchar(100)NOTNULLCOMMENT商品名称(快照),product_imagevarchar(500)DEFAULTNULLCOMMENT商品图片(快照),pricedecimal(10,2)NOTNULLCOMMENT单价,quantityintNOTNULLCOMMENT数量,subtotaldecimal(10,2)NOTNULLCOMMENT小计,PRIMARYKEY(id),KEYidx_order_id(order_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT订单明细表;设计要点command_id 唯一约束防重下单的基石cart_id 唯一约束一个购物车只能生成一个订单order_item 中存商品快照订单生成后商品信息可能被修改快照保证历史数据准确expire_time支付超时时间定时任务扫描使用四、下单流程完整实现ServicepublicclassOrderService{AutowiredprivateOrderMapperorderMapper;AutowiredprivateOrderItemMapperorderItemMapper;AutowiredprivateStockFeignClientstockFeignClient;AutowiredprivateProductFeignClientproductFeignClient;/** * 从购物车生成订单 */GlobalTransactional// Seata 分布式事务publicOrderResultcreateOrderFromCart(Cartcart){StringcommandIdcart.getCommandId();// 设备关门指令ID// 防重检查 OrderexistingorderMapper.selectOne(newLambdaQueryWrapperOrder().eq(Order::getCommandId,commandId));if(existing!null){log.warn(重复下单commandId{}返回已有订单,commandId);returnbuildResult(existing);}// 生成订单号 StringorderNoSnowflakeUtil.nextStr();// 创建订单 OrderordernewOrder();order.setId(SnowflakeUtil.nextId());order.setOrderNo(orderNo);order.setUserId(cart.getUserId());order.setDeviceId(cart.getDeviceId());order.setCartId(cart.getId());order.setCommandId(commandId);order.setStatus(OrderStatus.PENDING.getCode());// 解析购物车中的商品ListCartItemcartItemsJSONUtil.toList(cart.getItems().toString(),CartItem.class);BigDecimaltotalAmountBigDecimal.ZERO;for(CartItemitem:cartItems){// 获取商品最新价格防止购物车开启期间价格变更ProductproductproductFeignClient.getById(item.getProductId()).getData();BigDecimalunitPriceproduct.getPrice();BigDecimalsubtotalunitPrice.multiply(BigDecimal.valueOf(item.getQuantity()));totalAmounttotalAmount.add(subtotal);// 写入订单明细OrderItemorderItemnewOrderItem();orderItem.setId(SnowflakeUtil.nextId());orderItem.setOrderId(order.getId());orderItem.setProductId(item.getProductId());orderItem.setProductName(product.getName());orderItem.setProductImage(product.getMainImage());orderItem.setPrice(unitPrice);orderItem.setQuantity(item.getQuantity());orderItem.setSubtotal(subtotal);orderItemMapper.insert(orderItem);}order.setTotalAmount(totalAmount);order.setPayAmount(totalAmount);order.setStatus(OrderStatus.UNPAID.getCode());order.setExpireTime(LocalDateTime.now().plusMinutes(30));// 30分钟支付窗口orderMapper.insert(order);// 扣减真实库存Feign调用库存服务DeductStockDTOdeductDTOnewDeductStockDTO();deductDTO.setDeviceId(cart.getDeviceId());deductDTO.setOrderId(order.getId());deductDTO.setItems(cartItems.stream().map(i-{DeductStockDTO.ItemDTOdtonewDeductStockDTO.ItemDTO();dto.setProductId(i.getProductId());dto.setQuantity(i.getQuantity());returndto;}).collect(Collectors.toList()));stockFeignClient.deduct(deductDTO);// 更新购物车状态 cart.setStatus(DONE);cart.setOrderId(order.getId());cartMapper.updateById(cart);log.info(订单创建成功: orderNo{}, amount{}, items{},orderNo,totalAmount,cartItems.size());returnbuildResult(order);}}五、防重下单设计三层防护第一层commandId 唯一约束设备关门指令commandId是全局唯一的。DB 层设置UNIQUE KEY uk_command_id(command_id)即使网络重试导致重复请求第二个 INSERT 直接报DuplicateKeyException返回已有订单。第二层cartId 唯一约束一个购物车只能对应一个订单。UNIQUE KEY uk_cart_id(cart_id)保证同一购物车不会被两次生成订单。第三层幂等检查publicOrderResultcreateOrGetOrder(StringcommandId){OrderexistingorderMapper.selectByCommandId(commandId);if(existing!null){returnbuildResult(existing);// 已存在直接返回}returnlockAndCreate(commandId);// 加分布式锁后创建}六、雪花算法生成订单号publicclassSnowflakeUtil{privatestaticfinalSnowflakeSNOWFLAKE;static{// workerId 从 Nacos 配置中心读取确保每个实例唯一longworkerIdLong.parseLong(System.getProperty(snowflake.workerId,1));longdatacenterId1L;SNOWFLAKEIdUtil.createSnowflake(workerId,datacenterId);}publicstaticlongnextId(){returnSNOWFLAKE.nextId();}publicstaticStringnextStr(){returnSNOWFLAKE.nextIdStr();}}雪花算法特点全局唯一分布式环境下不会有重复趋势递增64位中高位是时间戳天然有序高性能纯内存计算QPS可达百万级七、订单与库存的分布式事务GlobalTransactional(nameorder_create,timeoutMills300000)publicOrderResultcreateOrderFromCart(Cartcart){// Seata AT 模式自动管理// 1. 本地事务创建订单// 2. Feign 远程调用库存服务扣减库存// 3. 任一环节失败自动回滚所有数据源}Seata AT 模式下GlobalTransactional注解会协调两个本地事务订单库的INSERT和库存库的UPDATE。回滚时自动执行 undo log。八、订单超时关闭Scheduled(fixedDelay60000)// 每分钟执行publicvoidcloseTimeoutOrders(){ListOrdertimeoutOrdersorderMapper.selectList(newLambdaQueryWrapperOrder().eq(Order::getStatus,OrderStatus.UNPAID.getCode()).lt(Order::getExpireTime,LocalDateTime.now()));for(Orderorder:timeoutOrders){try{cancelOrder(order.getId(),支付超时系统自动取消);}catch(Exceptione){log.error(超时关闭订单失败: {},order.getId(),e);}}}TransactionalpublicvoidcancelOrder(LongorderId,Stringreason){OrderorderorderMapper.selectById(orderId);if(!OrderStatus.canTransition(order.getStatus(),OrderStatus.CANCELLED)){thrownewBusinessException(当前订单状态不允许取消);}order.setStatus(OrderStatus.CANCELLED.getCode());order.setCancelTime(LocalDateTime.now());order.setCancelReason(reason);orderMapper.updateById(order);// 释放库存stockFeignClient.release(order.getDeviceId(),order.getId());}九、支付回调幂等性PostMapping(/callback/wechat)publicStringwechatPayCallback(RequestBodyWechatPayNotifyDTOdto){StringtransactionIddto.getTransactionId();// 幂等性已处理过直接返回成功避免微信重复回调OrderorderorderMapper.selectByTransactionId(transactionId);if(order!nullorder.getStatus()OrderStatus.PAID.getCode()){returnsuccess();// 重复通知直接返回成功}// 验签 更新订单状态if(!wechatPayService.verify(dto)){returnfail(验签失败);}orderorderMapper.selectByOrderNo(dto.getOutTradeNo());order.setStatus(OrderStatus.PAID.getCode());order.setPayTime(LocalDateTime.now());order.setPayChannel(WECHAT);order.setTransactionId(transactionId);orderMapper.updateById(order);// 订单完成order.setStatus(OrderStatus.COMPLETED.getCode());orderMapper.updateById(order);returnsuccess();}十、订单幂等性总结环节幂等机制实现下单commandId 唯一约束DB UNIQUE KEY库存扣减订单已存在则跳过先查后扣支付回调transactionId 去重DB 状态判断退款refundId 唯一约束DB UNIQUE KEY订单模块是整个系统中最后一道门。状态机设计清楚了防重和幂等做扎实了后面就是关门→付钱→走人的愉快体验。反之如果这里出了问题——重复扣款、库存超卖、支付成功订单没更新——那就是线上事故级别的灾难。