支付对接:微信/支付宝支付、回调解析、订单核销
钱货两讫,天经地义——但在无人售货柜里,"钱"这一步可比你想的复杂多了。
一、支付在无人售货柜中的完整流程
先说清楚整个支付链路,搞不懂全貌写出来的代码一定是漏的。
无人售货柜的支付流程和普通电商不同,它不是"先下单再付款",而是"先拿货再付款"。完整链路如下:
关门 → 视觉识别商品 → 生成订单 → 用户支付 → 支付平台回调 → 核销订单 → 交易完成 ↓ (失败)→ 超时关单/退款核心区别在于:订单是在用户拿完商品关门后才生成的,用户扫码开门时不需要支付,关门瞬间由视觉识别系统判断拿了什么,生成订单后调起支付。
二、微信支付对接
2.1 JSAPI支付(小程序场景)
无人售货柜多配套微信小程序,所以微信支付主要走JSAPI支付。流程是:
- 后端调用微信「统一下单」接口,拿到预支付会话标识
prepay_id - 后端用
prepay_id生成前端调起支付所需参数 - 前端调用
wx.requestPayment拉起支付界面 - 用户输入密码/指纹完成支付
- 微信服务器异步通知后端回调接口
2.2 统一下单核心代码
@Service@Slf4jpublicclassWechatPayService{@AutowiredprivateWxPayConfigwxPayConfig;/** * 统一下单,生成小程序调起支付参数 */publicWxPayJsapiResultunifiedOrder(Orderorder){try{// 1. 构建统一下单请求WxPayUnifiedOrderRequestrequest=newWxPayUnifiedOrderRequest();request.setAppid(wxPayConfig.getAppId());request.setMchId(wxPayConfig.getMchId());request.setNonceStr(WxPayKit.generateNonceStr());request.setBody("无人售货柜-"+order.getOrderNo());request.setOutTradeNo(order.getOrderNo());// 金额单位:分request.setTotalFee(order.getPayAmount().multiply(BigDecimal.valueOf(100)).intValue());request.setSpbillCreateIp(IpUtil.getClientIp());request.setNotifyUrl(wxPayConfig.getNotifyUrl());request.setTradeType("JSAPI");// 用户openid(小程序登录后获取)request.setOpenid(order.getOpenid());// 2. 签名request.setSign(WxPayKit.createSign(BeanUtil.beanToTreeMap(request),wxPayConfig.getApiKey()));// 3. 调用微信统一下单接口Stringxml=WxPayKit.toXml(request);Stringresult=HttpUtil.post(wxPayConfig.getUnifiedOrderUrl(),xml);Map<String,String>resultMap=WxPayKit.xmlToMap(result);// 4. 校验返回结果StringreturnCode=resultMap.get("return_code");StringresultCode=resultMap.get("result_code");if(!"SUCCESS".equals(returnCode)||!"SUCCESS".equals(resultCode)){log.error("微信统一下单失败: {}",resultMap);thrownewBusinessException("微信支付下单失败");}// 5. 生成前端调起支付参数StringprepayId=resultMap.get("prepay_id");WxPayJsapiResultjsapiResult=newWxPayJsapiResult();jsapiResult.setAppId(wxPayConfig.getAppId());jsapiResult.setTimeStamp(String.valueOf(System.currentTimeMillis()/1000));jsapiResult.setNonceStr(WxPayKit.generateNonceStr());jsapiResult.setPackage("prepay_id="+prepayId);jsapiResult.setSignType("RSA");// 支付签名jsapiResult.setPaySign(WxPayKit.createPaySign(jsapiResult.getAppId(),jsapiResult.getTimeStamp(),jsapiResult.getNonceStr(),jsapiResult.getPackage(),wxPayConfig.getApiKey()));returnjsapiResult;}catch(Exceptione){log.error("微信统一下单异常, orderNo={}",order.getOrderNo(),e);thrownewBusinessException("支付服务异常");}}}新手提醒:微信支付金额单位是分,不是元。
BigDecimal转换时一定要乘以100,这个坑踩过的人都知道。
三、支付宝对接
3.1 当面付(扫码场景)
支付宝这边我们用「当面付」(F2FPay),适用于用户扫码直接支付的流程。支付宝的SDK封装得比较友好,用alipay-sdk-java即可。
@Service@Slf4jpublicclassAlipayService{@AutowiredprivateAlipayConfigalipayConfig;/** * 当面付预创建交易(生成二维码) */publicAlipayTradePrecreateResultprecreate(Orderorder){try{AlipayClientclient=newDefaultAlipayClient(alipayConfig.getGateway(),alipayConfig.getAppId(),alipayConfig.getPrivateKey(),"json","UTF-8",alipayConfig.getAlipayPublicKey(),"RSA2");AlipayTradePrecreateRequestrequest=newAlipayTradePrecreateRequest();request.setNotifyUrl(alipayConfig.getNotifyUrl());Map<String,Object>bizContent=newHashMap<>();bizContent.put("out_trade_no",order.getOrderNo());bizContent.put("total_amount",order.getPayAmount().toString());bizContent.put("subject","无人售货柜-"+order.getOrderNo());request.setBizContent(JSON.toJSONString(bizContent));AlipayTradePrecreateResponseresponse=client.execute(request);if(!response.isSuccess()){log.error("支付宝预下单失败: {}",response.getSubMsg());thrownewBusinessException("支付宝支付下单失败");}AlipayTradePrecreateResultresult=newAlipayTradePrecreateResult();result.setQrCode(response.getQrCode());result.setTradeNo(response.getTradeNo());returnresult;}catch(AlipayApiExceptione){log.error("支付宝预下单异常",e);thrownewBusinessException("支付服务异常");}}}四、支付回调处理(核心中的核心)
4.1 回调处理流程
支付回调是整个支付链路最关键的一环,处理不好就是资损。流程如下:
支付平台异步通知 → 验签 → 金额校验 → 幂等判断 → 更新订单状态 → 扣减库存 → 记录流水 → 返回SUCCESS4.2 回调处理Service完整实现
@Service@Slf4jpublicclassPayCallbackService{@AutowiredprivateOrderServiceorderService;@AutowiredprivatePaymentServicepaymentService;@AutowiredprivateInventoryServiceinventoryService;@AutowiredprivateRedissonClientredissonClient;/** * 微信支付回调处理 */publicStringhandleWechatCallback(StringxmlData){try{Map<String,String>params=WxPayKit.xmlToMap(xmlData);// 1. 验签 —— 防止伪造回调if(!WxPayKit.verifySign(params,wxPayConfig.getApiKey())){log.error("微信回调验签失败");return"<xml><return_code>FAIL</return_code></xml>";}// 2. 解析回调数据StringorderNo=params.get("out_trade_no");StringtransactionId=params.get("transaction_id");StringtotalFee=params.get("total_fee");StringresultCode=params.get("result_code");if(!"SUCCESS".equals(resultCode)){log.error("微信回调支付失败, orderNo={}",orderNo);return"<xml><return_code>SUCCESS</return_code></xml>";}// 3. 金额校验 —— 防止金额被篡改Orderorder=orderService.getByOrderNo(orderNo);if(order==null){log.error("订单不存在, orderNo={}",orderNo);return"<xml><return_code>SUCCESS</return_code></xml>";}intexpectedFee=order.getPayAmount().multiply(BigDecimal.valueOf(100)).intValue();if(expectedFee!=Integer.parseInt(totalFee)){log.error("金额不一致, 期望={}分, 实际={}分",expectedFee,totalFee);// 记录异常,人工介入paymentService.recordAmountMismatch(orderNo,expectedFee,totalFee);return"<xml><return_code>SUCCESS</return_code></xml>";}// 4. 幂等处理 —— 分布式锁防重复处理RLocklock=redissonClient.getLock("pay:callback:"+orderNo);if(!lock.tryLock(0,30,TimeUnit.SECONDS)){log.warn("回调重复到达, 已有处理中, orderNo={}",orderNo);return"<xml><return_code>SUCCESS</return_code></xml>";}try{// 再次检查订单状态(双重幂等)if(order.getStatus()!=OrderStatus.UNPAID){log.info("订单已处理, 跳过, orderNo={}, status={}",orderNo,order.getStatus());return"<xml><return_code>SUCCESS</return_code></xml>";}// 5. 更新订单状态orderService.paySuccess(orderNo,transactionId);// 6. 确认扣减库存(预占 → 确认)inventoryService.confirmDeduct(order);// 7. 记录支付流水Paymentpayment=newPayment();payment.setOrderNo(orderNo);payment.setTransactionId(transactionId);payment.setPayType(PayType.WECHAT);payment.setAmount(order.getPayAmount());payment.setStatus(PaymentStatus.SUCCESS);payment.setCallbackTime(LocalDateTime.now());paymentService.save(payment);log.info("微信支付回调处理成功, orderNo={}",orderNo);return"<xml><return_code>SUCCESS</return_code></xml>";}finally{lock.unlock();}}catch(Exceptione){log.error("微信回调处理异常",e);return"<xml><return_code>FAIL</return_code></xml>";}}}4.3 幂等性设计详解
支付平台会重试回调(微信最多通知15次),所以幂等性是生命线。实现方式:
| 层级 | 方案 | 说明 |
|---|---|---|
| 第一层 | 分布式锁 | Redisson锁同一订单号,防止并发处理 |
| 第二层 | 状态判断 | 订单非UNPAID状态直接返回SUCCESS |
| 第三层 | 流水号去重 | transaction_id唯一索引,防止重复入账 |
三层防护,缺一不可。
五、支付状态查询兜底
回调可能因网络问题丢失,所以必须有主动查询兜底机制:
/** * 定时扫描未确认支付的订单,主动查询支付状态 */@Scheduled(cron="0 */5 * * * ?")publicvoidcheckUnpaidOrders(){// 查询创建超过5分钟但仍未支付的订单List<Order>orders=orderService.findUnpaidOrders(5);for(Orderorder:orders){try{// 主动查询微信/支付宝支付状态PayStatusResultresult=payQueryService.queryPayStatus(order);if(result.isPaid()){// 主动补偿:走和回调一样的逻辑payCallbackService.processPaySuccess(order,result.getTransactionId());}elseif(result.isExpired()){// 支付已过期,关闭订单orderService.closeOrder(order.getOrderNo(),"支付超时关闭");inventoryService.releasePreempt(order);}}catch(Exceptione){log.error("查询支付状态异常, orderNo={}",order.getOrderNo(),e);}}}六、退款流程
退款不是简单的"把钱还回去",涉及多步状态流转:
发起退款 → 第三方处理 → 退款回调 → 更新订单(已退款) → 恢复库存 → 通知用户/** * 发起微信退款 */publicvoidrefund(Orderorder,Stringreason){// 1. 创建退款记录RefundRecordrefund=newRefundRecord();refund.setOrderNo(order.getOrderNo());refund.setRefundNo(IdUtil.generateRefundNo());refund.setAmount(order.getPayAmount());refund.setReason(reason);refund.setStatus(RefundStatus.PROCESSING);refundRecordService.save(refund);// 2. 调用微信退款接口WxPayRefundRequestrequest=newWxPayRefundRequest();request.setOutTradeNo(order.getOrderNo());request.setOutRefundNo(refund.getRefundNo());request.setTotalFee(order.getPayAmount().multiply(BigDecimal.valueOf(100)).intValue());request.setRefundFee(refund.getAmount().multiply(BigDecimal.valueOf(100)).intValue());request.setNotifyUrl(wxPayConfig.getRefundNotifyUrl());WxPayRefundResponseresponse=wxPayService.refund(request);if(!response.isSuccess()){thrownewBusinessException("退款发起失败: "+response.getErrMsg());}// 退款结果由异步回调通知}七、支付流水表设计
CREATETABLEpayment(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_noVARCHAR(64)NOTNULLCOMMENT'订单号',transaction_idVARCHAR(64)COMMENT'第三方流水号',refund_noVARCHAR(64)COMMENT'退款单号',pay_typeTINYINTNOTNULLCOMMENT'支付方式:1微信2支付宝',amountDECIMAL(10,2)NOTNULLCOMMENT'支付金额',statusTINYINTNOTNULLCOMMENT'状态:0待支付1成功2失败3已退款',callback_timeDATETIMECOMMENT'回调时间',create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP,INDEXidx_order_no(order_no),INDEXidx_transaction_id(transaction_id),UNIQUEINDEXuk_transaction_id(transaction_id))COMMENT'支付流水表';
transaction_id加唯一索引,既是查询索引又是去重保障,一箭双雕。
八、安全设计要点
| 安全风险 | 防护方案 |
|---|---|
| 伪造回调 | 回调验签(微信MD5/HMAC-SHA256,支付宝RSA2) |
| 金额篡改 | 回调金额与订单金额比对校验 |
| 重放攻击 | 校验回调时间戳,超时拒绝 |
| 重复入账 | 分布式锁 + 状态判断 + 流水号唯一索引 |
| 退款超退 | 退款金额校验,不超过原支付金额 |
九、常见问题排查
回调网络超时:微信要求5秒内返回响应,回调处理逻辑中不要做耗时操作。库存扣减走异步MQ,回调只做核心状态更新。
金额不一致:最危险的情况。回调金额与订单金额不符时,不要自动处理,记录异常流水转人工审核。
重复回调:幂等设计兜底,重复回调直接返回SUCCESS即可,微信收到SUCCESS就不再重试。但如果你返回FAIL,微信会疯狂重试15次,系统压力陡增。
支付了但订单没更新:这就是支付状态查询兜底机制存在的意义——定时任务主动查,不依赖回调。
支付对接看着简单,坑全在细节里。验签、幂等、金额校验、兜底查询,每一环都是血的教训换来的。把这几块设计扎实了,后面不管是加新的支付渠道还是处理退款,都是照猫画虎的事。