diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java index 0a8e30d..51b23b1 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java +++ b/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; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ReflectUtil; @@ -103,13 +104,9 @@ public class SocialClientServiceImpl implements SocialClientService { public String miniprogramState; /** - * 上传发货信息重试次数 + * 上传发货信息重试间隔(毫秒),数组长度即重试次数;总等待最坏 1 + 2 + 4 = 7 秒,覆盖微信支付回调的常见延迟 */ - private static final int UPLOAD_SHIPPING_INFO_MAX_RETRIES = 5; - /** - * 上传发货信息重试间隔 - */ - private static final Duration UPLOAD_SHIPPING_INFO_RETRY_INTERVAL = Duration.ofMillis(500L); + private static final long[] UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS = {1000, 2000, 4000}; /** * 微信错误码:支付单不存在 */ @@ -383,31 +380,23 @@ public class SocialClientServiceImpl implements SocialClientService { .build(); // 重试机制:解决支付回调与订单信息上传之间的时间差导致的 10060001 错误 // 对应 ISSUE:https://gitee.com/zhijiantianya/yudao-cloud/pulls/230 - for (int attempt = 1; attempt <= UPLOAD_SHIPPING_INFO_MAX_RETRIES; attempt++) { + // 注意:wx-java 的 upload 内部对 errCode != 0 直接抛 WxErrorException,所以重试判断必须基于异常的 errorCode + int maxAttempts = UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS.length + 1; + for (int attempt = 1; attempt <= maxAttempts; attempt++) { try { WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request); - // 成功,直接返回 - if (response.getErrCode() == 0) { - log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功:request({}) response({})]", request, response); - return; - } - // 如果是 10060001 错误(支付单不存在)且还有重试次数,则等待后重试 - if (response.getErrCode() == WX_ERR_CODE_PAY_ORDER_NOT_EXIST && attempt < UPLOAD_SHIPPING_INFO_MAX_RETRIES) { - log.warn("[uploadWxaOrderShippingInfo][第 {} 次尝试失败,支付单不存在,{} 后重试:request({}) response({})]", - attempt, UPLOAD_SHIPPING_INFO_RETRY_INTERVAL, request, response); - Thread.sleep(UPLOAD_SHIPPING_INFO_RETRY_INTERVAL.toMillis()); + log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功:request({}) response({})]", request, response); + return; + } catch (WxErrorException ex) { + if (ex.getError().getErrorCode() == WX_ERR_CODE_PAY_ORDER_NOT_EXIST && attempt < maxAttempts) { + long delayMillis = UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS[attempt - 1]; + log.warn("[uploadWxaOrderShippingInfo][第 {} 次尝试失败,支付单不存在,{} ms 后重试:request({})]", + attempt, delayMillis, request, ex); + ThreadUtil.sleep(delayMillis); continue; } - // 其他错误或重试次数用尽,抛出异常 - log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({}) response({})]", request, response); - throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, response.getErrMsg()); - } catch (WxErrorException ex) { log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({})]", request, ex); throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, ex.getError().getErrorMsg()); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - log.error("[uploadWxaOrderShippingInfo][重试等待被中断:request({})]", request, ex); - throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, "重试等待被中断"); } } } diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java index ae87de0..a4c85f7 100644 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java @@ -1,13 +1,17 @@ package cn.iocoder.yudao.module.system.service.social; +import cn.binarywang.wx.miniapp.api.WxMaOrderShippingService; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaUserService; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.binarywang.wx.miniapp.bean.shop.request.shipping.WxMaOrderShippingInfoUploadRequest; +import cn.binarywang.wx.miniapp.bean.shop.response.WxMaOrderShippingInfoBaseResponse; import cn.hutool.core.util.ReflectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO; import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; @@ -18,6 +22,7 @@ import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; import jakarta.annotation.Resource; import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.zhyd.oauth.config.AuthConfig; @@ -26,6 +31,7 @@ import me.zhyd.oauth.model.AuthUser; import me.zhyd.oauth.request.AuthDefaultRequest; import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.utils.AuthStateUtils; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.springframework.context.annotation.Import; @@ -468,4 +474,100 @@ public class SocialClientServiceImplTest extends BaseDbUnitTest { assertPojoEquals(dbSocialClient, pageResult.getList().get(0)); } + // =================== 微信小程序订单发货 =================== + + @BeforeAll + public static void setUpUploadShippingBackoff() { + // 测试场景下把退避数组的每一项改为 1ms,避免拖慢用例(数组引用是 final 但元素可改) + long[] backoff = (long[]) ReflectUtil.getFieldValue(SocialClientServiceImpl.class, + "UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS"); + for (int i = 0; i < backoff.length; i++) { + backoff[i] = 1L; + } + } + + @Test + public void testUploadWxaOrderShippingInfo_success() throws WxErrorException { + // 准备参数 + Integer userType = randomPojo(UserTypeEnum.class).getValue(); + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO(); + // mock 方法:首次调用就成功 + WxMaOrderShippingService shippingService = mockWxMaOrderShippingService(); + when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class))) + .thenReturn(new WxMaOrderShippingInfoBaseResponse()); + + // 调用 + socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO); + // 断言:仅调用 1 次,无重试 + verify(shippingService, times(1)).upload(any(WxMaOrderShippingInfoUploadRequest.class)); + } + + @Test + public void testUploadWxaOrderShippingInfo_retryThenSuccess() throws WxErrorException { + // 准备参数 + Integer userType = randomPojo(UserTypeEnum.class).getValue(); + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO(); + // mock 方法:首次抛 10060001,第二次成功 + WxMaOrderShippingService shippingService = mockWxMaOrderShippingService(); + when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class))) + .thenThrow(buildWxErrorException(10060001)) + .thenReturn(new WxMaOrderShippingInfoBaseResponse()); + + // 调用 + socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO); + // 断言:上传调用了 2 次,触发了 1 次重试 + verify(shippingService, times(2)).upload(any(WxMaOrderShippingInfoUploadRequest.class)); + } + + @Test + public void testUploadWxaOrderShippingInfo_retryExhausted() throws WxErrorException { + // 准备参数 + Integer userType = randomPojo(UserTypeEnum.class).getValue(); + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO(); + // mock 方法:始终抛 10060001 + WxMaOrderShippingService shippingService = mockWxMaOrderShippingService(); + when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class))) + .thenThrow(buildWxErrorException(10060001)); + + // 调用并断言:重试用尽抛业务异常 + assertServiceException(() -> socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO), + SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR); + // 断言:共 4 次尝试(1 次首发 + 3 次重试) + verify(shippingService, times(4)).upload(any(WxMaOrderShippingInfoUploadRequest.class)); + } + + @Test + public void testUploadWxaOrderShippingInfo_otherErrorNoRetry() throws WxErrorException { + // 准备参数 + Integer userType = randomPojo(UserTypeEnum.class).getValue(); + SocialWxaOrderUploadShippingInfoReqDTO reqDTO = randomUploadShippingReqDTO(); + // mock 方法:抛非 10060001 错误(如 access_token 失效) + WxMaOrderShippingService shippingService = mockWxMaOrderShippingService(); + when(shippingService.upload(any(WxMaOrderShippingInfoUploadRequest.class))) + .thenThrow(buildWxErrorException(40001)); + + // 调用并断言:立即抛业务异常,无重试 + assertServiceException(() -> socialClientService.uploadWxaOrderShippingInfo(userType, reqDTO), + SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR); + verify(shippingService, times(1)).upload(any(WxMaOrderShippingInfoUploadRequest.class)); + } + + /** 构造一个发货上传请求 */ + private SocialWxaOrderUploadShippingInfoReqDTO randomUploadShippingReqDTO() { + return randomPojo(SocialWxaOrderUploadShippingInfoReqDTO.class, + o -> o.setLogisticsType(SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_EXPRESS)); + } + + /** mock 出 wxMaService.getWxMaOrderShippingService() 的返回值并返回该 mock */ + private WxMaOrderShippingService mockWxMaOrderShippingService() { + WxMaOrderShippingService shippingService = mock(WxMaOrderShippingService.class); + when(wxMaService.getWxMaOrderShippingService()).thenReturn(shippingService); + return shippingService; + } + + /** 构造指定 errorCode 的 WxErrorException */ + private WxErrorException buildWxErrorException(int errorCode) { + return new WxErrorException(WxError.builder().errorCode(errorCode).errorMsg("mock error").build()); + } + }