正常情况下我们选择商品然后购买,完成支付后就发货了,但是因为大多采用的是http协议,如果出现了网络异常等情况就会掉单之类的,比如顾客付完钱了但是订单状态并没有改变,还有就是支付成功后回调失败,导致没有发货。对于真正的服务,这些异常情况我们也得考虑进去。
上面基本就是订单服务的主流程了,但是我们可以思考一下异常流程:
- 用户订单创建成功,但创建支付单 HTTP 超时失败。
- 支付回调时,系统宕机或者本身服务出问题。
- 支付成功后发送MQ消息,消息丢失,用户支付掉单。
- 长时间未支付,超时订单。
那么,这些就都是可能出现的异常流程。虽然概率很低,但随着使用规模的增加,很低概率的问题,也会产生较大规模的客诉问题。所以要针对这些流程做补偿处理。
- 针对1~4提到异常流程,一条支付链路就会被扩展为现在的样子,在各个流程中需要穿插进入异常补偿流程。
- 用户下单,但可能存在之前下的残单,那么就要对应给予补充的流程后,再返回回去。
- 支付回调,仍然可能有异常。所以要有掉单补偿和发货补偿。两条任务处理。
因此,我们通过使用定时任务完成补偿
1. 掉单补偿,检测未接收到或未正确处理的支付回调通知(即用户已经扫码支付成功但是订单状态没有改变)
@Component @Slf4j public class NoPayNotifyOrderJob { @Resource private IOrderRepository orderRepository; @Resource private IOrderService orderService; @Resource private RabbitTemplate rabbitTemplate; @Value("${pay.alipay.APP_ID}") private String APP_ID; @Value("${pay.alipay.APP_PRIVATE_KEY}") private String APP_PRIVATE_KEY; @Value("${pay.alipay.ALIPAY_PUBLIC_KEY}") private String ALIPAY_PUBLIC_KEY; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); @Timed(value = "no_pay_notify_order_job", description = "定时任务,订单支付状态更新") @Scheduled(cron = "0/3 * * * * ?") public void exec(){ try { List<String> orderIds = orderRepository.queryNoPayNotifyOrder(); if (orderIds.isEmpty()) { log.info("定时任务,订单支付状态更新,暂无未更新订单 orderId is null"); return; } for (String orderId: orderIds) { AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE); AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", orderId); request.setBizContent(bizContent.toString()); AlipayTradeQueryResponse response = null; try { response = alipayClient.execute(request); if (!response.isSuccess()) { new ChatGPTException("请求支付查询查询失败"); } } catch (AlipayApiException e) { log.error("请求支付宝查询支付结果异常:{}", e.toString(), e); new ChatGPTException("请求支付查询查询失败"); } //获取支付结果 String resultJson = response.getBody(); //转map Map resultMap = JSON.parseObject(resultJson, Map.class); Map alipay_trade_query_response = (Map) resultMap.get("alipay_trade_query_response"); //支付结果 Double total_amount = Double.valueOf(alipay_trade_query_response.get("total_amount").toString()); String trade_no = (String) alipay_trade_query_response.get("trade_no"); String successTime = (String)alipay_trade_query_response.get("gmt_payment"); boolean isSuccess = orderService.changeOrderPaySuccess(orderId, trade_no, BigDecimal.valueOf(total_amount), dateFormat.parse(successTime)); if (isSuccess) { // 发布消息 rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT,"", orderId); } } }catch (Exception e){ log.error("定时任务,订单支付状态更新失败", e); e.printStackTrace(); } } }
2.订单补货任务
@Component @Slf4j public class OrderReplenishmentJob { @Resource private IOrderService orderService; @Resource private RabbitTemplate rabbitTemplate; /** * 执行订单补货,超时3分钟,已支付,待发货未发货的订单 */ @Scheduled(cron = "0 0/3 * * * ?") public void exec() { try { List<String> orderIds = orderService.queryReplenishmentOrder(); if (orderIds.isEmpty()) { log.info("定时任务,订单补货不存在,查询 orderIds is null"); return; } for (String orderId : orderIds) { log.info("定时任务,订单补货开始。orderId: {}", orderId); rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT,"", orderId); } } catch (Exception e) { log.error("定时任务,订单补货失败。", e); } } }
3. 超时关单任务
@Component @Slf4j public class TimeoutCloseOrderJob { @Resource private IOrderService orderService; @Scheduled(cron = "0 0/30 * * * ?") public void exec() { try { List<String> orderIds = orderService.queryTimeoutCloseOrderList(); for (String orderId: orderIds) { boolean status = orderService.changeOrderClose(orderId); log.info("定时任务,超时30分钟订单关闭 orderId: {} status:{}", orderId, status); } }catch (Exception e) { log.error("定时任务,超时15分钟订单关闭失败", e); } } }