Commit 987d49c66836199dfd165f272147806e55b7f8ab

Authored by YunaiV
1 parent 0b650739

fix(social):修复微信小程序订单发货信息上传重试机制从未生效的 bug,改为指数退避并补单测

yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
@@ -12,6 +12,7 @@ import cn.binarywang.wx.miniapp.constant.WxMaConstants; @@ -12,6 +12,7 @@ import cn.binarywang.wx.miniapp.constant.WxMaConstants;
12 import cn.hutool.core.bean.BeanUtil; 12 import cn.hutool.core.bean.BeanUtil;
13 import cn.hutool.core.collection.CollUtil; 13 import cn.hutool.core.collection.CollUtil;
14 import cn.hutool.core.lang.Assert; 14 import cn.hutool.core.lang.Assert;
  15 +import cn.hutool.core.thread.ThreadUtil;
15 import cn.hutool.core.util.DesensitizedUtil; 16 import cn.hutool.core.util.DesensitizedUtil;
16 import cn.hutool.core.util.ObjUtil; 17 import cn.hutool.core.util.ObjUtil;
17 import cn.hutool.core.util.ReflectUtil; 18 import cn.hutool.core.util.ReflectUtil;
@@ -103,13 +104,9 @@ public class SocialClientServiceImpl implements SocialClientService { @@ -103,13 +104,9 @@ public class SocialClientServiceImpl implements SocialClientService {
103 public String miniprogramState; 104 public String miniprogramState;
104 105
105 /** 106 /**
106 - * 上传发货信息重试次数 107 + * 上传发货信息重试间隔(毫秒),数组长度即重试次数;总等待最坏 1 + 2 + 4 = 7 秒,覆盖微信支付回调的常见延迟
107 */ 108 */
108 - private static final int UPLOAD_SHIPPING_INFO_MAX_RETRIES = 5;  
109 - /**  
110 - * 上传发货信息重试间隔  
111 - */  
112 - private static final Duration UPLOAD_SHIPPING_INFO_RETRY_INTERVAL = Duration.ofMillis(500L); 109 + private static final long[] UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS = {1000, 2000, 4000};
113 /** 110 /**
114 * 微信错误码:支付单不存在 111 * 微信错误码:支付单不存在
115 */ 112 */
@@ -383,31 +380,23 @@ public class SocialClientServiceImpl implements SocialClientService { @@ -383,31 +380,23 @@ public class SocialClientServiceImpl implements SocialClientService {
383 .build(); 380 .build();
384 // 重试机制:解决支付回调与订单信息上传之间的时间差导致的 10060001 错误 381 // 重试机制:解决支付回调与订单信息上传之间的时间差导致的 10060001 错误
385 // 对应 ISSUE:https://gitee.com/zhijiantianya/yudao-cloud/pulls/230 382 // 对应 ISSUE:https://gitee.com/zhijiantianya/yudao-cloud/pulls/230
386 - for (int attempt = 1; attempt <= UPLOAD_SHIPPING_INFO_MAX_RETRIES; attempt++) { 383 + // 注意:wx-java 的 upload 内部对 errCode != 0 直接抛 WxErrorException,所以重试判断必须基于异常的 errorCode
  384 + int maxAttempts = UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS.length + 1;
  385 + for (int attempt = 1; attempt <= maxAttempts; attempt++) {
387 try { 386 try {
388 WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request); 387 WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request);
389 - // 成功,直接返回  
390 - if (response.getErrCode() == 0) {  
391 - log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功:request({}) response({})]", request, response);  
392 - return;  
393 - }  
394 - // 如果是 10060001 错误(支付单不存在)且还有重试次数,则等待后重试  
395 - if (response.getErrCode() == WX_ERR_CODE_PAY_ORDER_NOT_EXIST && attempt < UPLOAD_SHIPPING_INFO_MAX_RETRIES) {  
396 - log.warn("[uploadWxaOrderShippingInfo][第 {} 次尝试失败,支付单不存在,{} 后重试:request({}) response({})]",  
397 - attempt, UPLOAD_SHIPPING_INFO_RETRY_INTERVAL, request, response);  
398 - Thread.sleep(UPLOAD_SHIPPING_INFO_RETRY_INTERVAL.toMillis()); 388 + log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功:request({}) response({})]", request, response);
  389 + return;
  390 + } catch (WxErrorException ex) {
  391 + if (ex.getError().getErrorCode() == WX_ERR_CODE_PAY_ORDER_NOT_EXIST && attempt < maxAttempts) {
  392 + long delayMillis = UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS[attempt - 1];
  393 + log.warn("[uploadWxaOrderShippingInfo][第 {} 次尝试失败,支付单不存在,{} ms 后重试:request({})]",
  394 + attempt, delayMillis, request, ex);
  395 + ThreadUtil.sleep(delayMillis);
399 continue; 396 continue;
400 } 397 }
401 - // 其他错误或重试次数用尽,抛出异常  
402 - log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({}) response({})]", request, response);  
403 - throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, response.getErrMsg());  
404 - } catch (WxErrorException ex) {  
405 log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({})]", request, ex); 398 log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({})]", request, ex);
406 throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, ex.getError().getErrorMsg()); 399 throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, ex.getError().getErrorMsg());
407 - } catch (InterruptedException ex) {  
408 - Thread.currentThread().interrupt();  
409 - log.error("[uploadWxaOrderShippingInfo][重试等待被中断:request({})]", request, ex);  
410 - throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, "重试等待被中断");  
411 } 400 }
412 } 401 }
413 } 402 }
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java
1 package cn.iocoder.yudao.module.system.service.social; 1 package cn.iocoder.yudao.module.system.service.social;
2 2
  3 +import cn.binarywang.wx.miniapp.api.WxMaOrderShippingService;
3 import cn.binarywang.wx.miniapp.api.WxMaService; 4 import cn.binarywang.wx.miniapp.api.WxMaService;
4 import cn.binarywang.wx.miniapp.api.WxMaUserService; 5 import cn.binarywang.wx.miniapp.api.WxMaUserService;
5 import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; 6 import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
  7 +import cn.binarywang.wx.miniapp.bean.shop.request.shipping.WxMaOrderShippingInfoUploadRequest;
  8 +import cn.binarywang.wx.miniapp.bean.shop.response.WxMaOrderShippingInfoBaseResponse;
6 import cn.hutool.core.util.ReflectUtil; 9 import cn.hutool.core.util.ReflectUtil;
7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 10 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
8 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 11 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
9 import cn.iocoder.yudao.framework.common.pojo.PageResult; 12 import cn.iocoder.yudao.framework.common.pojo.PageResult;
10 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 13 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  14 +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO;
11 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; 15 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
12 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; 16 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
13 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; 17 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
@@ -18,6 +22,7 @@ import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; @@ -18,6 +22,7 @@ import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
18 import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; 22 import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
19 import jakarta.annotation.Resource; 23 import jakarta.annotation.Resource;
20 import me.chanjar.weixin.common.bean.WxJsapiSignature; 24 import me.chanjar.weixin.common.bean.WxJsapiSignature;
  25 +import me.chanjar.weixin.common.error.WxError;
21 import me.chanjar.weixin.common.error.WxErrorException; 26 import me.chanjar.weixin.common.error.WxErrorException;
22 import me.chanjar.weixin.mp.api.WxMpService; 27 import me.chanjar.weixin.mp.api.WxMpService;
23 import me.zhyd.oauth.config.AuthConfig; 28 import me.zhyd.oauth.config.AuthConfig;
@@ -26,6 +31,7 @@ import me.zhyd.oauth.model.AuthUser; @@ -26,6 +31,7 @@ import me.zhyd.oauth.model.AuthUser;
26 import me.zhyd.oauth.request.AuthDefaultRequest; 31 import me.zhyd.oauth.request.AuthDefaultRequest;
27 import me.zhyd.oauth.request.AuthRequest; 32 import me.zhyd.oauth.request.AuthRequest;
28 import me.zhyd.oauth.utils.AuthStateUtils; 33 import me.zhyd.oauth.utils.AuthStateUtils;
  34 +import org.junit.jupiter.api.BeforeAll;
29 import org.junit.jupiter.api.Test; 35 import org.junit.jupiter.api.Test;
30 import org.mockito.MockedStatic; 36 import org.mockito.MockedStatic;
31 import org.springframework.context.annotation.Import; 37 import org.springframework.context.annotation.Import;
@@ -468,4 +474,100 @@ public class SocialClientServiceImplTest extends BaseDbUnitTest { @@ -468,4 +474,100 @@ public class SocialClientServiceImplTest extends BaseDbUnitTest {
468 assertPojoEquals(dbSocialClient, pageResult.getList().get(0)); 474 assertPojoEquals(dbSocialClient, pageResult.getList().get(0));
469 } 475 }
470 476
  477 + // =================== 微信小程序订单发货 ===================
  478 +
  479 + @BeforeAll
  480 + public static void setUpUploadShippingBackoff() {
  481 + // 测试场景下把退避数组的每一项改为 1ms,避免拖慢用例(数组引用是 final 但元素可改)
  482 + long[] backoff = (long[]) ReflectUtil.getFieldValue(SocialClientServiceImpl.class,
  483 + "UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS");
  484 + for (int i = 0; i < backoff.length; i++) {
  485 + backoff[i] = 1L;
  486 + }
  487 + }
  488 +
  489 + @Test
  490 + public void testUploadWxaOrderShippingInfo_success() throws WxErrorException {
  491 + // 准备参数
  492 + Integer userType = randomPojo(UserTypeEnum.class).getValue();
  493 + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO();
  494 + // mock 方法:首次调用就成功
  495 + WxMaOrderShippingService shippingService = mockWxMaOrderShippingService();
  496 + when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class)))
  497 + .thenReturn(new WxMaOrderShippingInfoBaseResponse());
  498 +
  499 + // 调用
  500 + socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO);
  501 + // 断言:仅调用 1 次,无重试
  502 + verify(shippingService, times(1)).upload(any(WxMaOrderShippingInfoUploadRequest.class));
  503 + }
  504 +
  505 + @Test
  506 + public void testUploadWxaOrderShippingInfo_retryThenSuccess() throws WxErrorException {
  507 + // 准备参数
  508 + Integer userType = randomPojo(UserTypeEnum.class).getValue();
  509 + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO();
  510 + // mock 方法:首次抛 10060001,第二次成功
  511 + WxMaOrderShippingService shippingService = mockWxMaOrderShippingService();
  512 + when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class)))
  513 + .thenThrow(buildWxErrorException(10060001))
  514 + .thenReturn(new WxMaOrderShippingInfoBaseResponse());
  515 +
  516 + // 调用
  517 + socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO);
  518 + // 断言:上传调用了 2 次,触发了 1 次重试
  519 + verify(shippingService, times(2)).upload(any(WxMaOrderShippingInfoUploadRequest.class));
  520 + }
  521 +
  522 + @Test
  523 + public void testUploadWxaOrderShippingInfo_retryExhausted() throws WxErrorException {
  524 + // 准备参数
  525 + Integer userType = randomPojo(UserTypeEnum.class).getValue();
  526 + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO();
  527 + // mock 方法:始终抛 10060001
  528 + WxMaOrderShippingService shippingService = mockWxMaOrderShippingService();
  529 + when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class)))
  530 + .thenThrow(buildWxErrorException(10060001));
  531 +
  532 + // 调用并断言:重试用尽抛业务异常
  533 + assertServiceException(() -> socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO),
  534 + SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR);
  535 + // 断言:共 4 次尝试(1 次首发 + 3 次重试)
  536 + verify(shippingService, times(4)).upload(any(WxMaOrderShippingInfoUploadRequest.class));
  537 + }
  538 +
  539 + @Test
  540 + public void testUploadWxaOrderShippingInfo_otherErrorNoRetry() throws WxErrorException {
  541 + // 准备参数
  542 + Integer userType = randomPojo(UserTypeEnum.class).getValue();
  543 + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO();
  544 + // mock 方法:抛非 10060001 错误(如 access_token 失效)
  545 + WxMaOrderShippingService shippingService = mockWxMaOrderShippingService();
  546 + when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class)))
  547 + .thenThrow(buildWxErrorException(40001));
  548 +
  549 + // 调用并断言:立即抛业务异常,无重试
  550 + assertServiceException(() -> socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO),
  551 + SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR);
  552 + verify(shippingService, times(1)).upload(any(WxMaOrderShippingInfoUploadRequest.class));
  553 + }
  554 +
  555 + /** 构造一个发货上传请求 */
  556 + private SocialWxaOrderUploadShippingInfoReqDTO randomUploadShippingReqDTO() {
  557 + return randomPojo(SocialWxaOrderUploadShippingInfoReqDTO.class,
  558 + o -> o.setLogisticsType(SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_EXPRESS));
  559 + }
  560 +
  561 + /** mock 出 wxMaService.getWxMaOrderShippingService() 的返回值并返回该 mock */
  562 + private WxMaOrderShippingService mockWxMaOrderShippingService() {
  563 + WxMaOrderShippingService shippingService = mock(WxMaOrderShippingService.class);
  564 + when(wxMaService.getWxMaOrderShippingService()).thenReturn(shippingService);
  565 + return shippingService;
  566 + }
  567 +
  568 + /** 构造指定 errorCode 的 WxErrorException */
  569 + private WxErrorException buildWxErrorException(int errorCode) {
  570 + return new WxErrorException(WxError.builder().errorCode(errorCode).errorMsg("mock error").build());
  571 + }
  572 +
471 } 573 }