Commit 987d49c66836199dfd165f272147806e55b7f8ab
1 parent
0b650739
fix(social):修复微信小程序订单发货信息上传重试机制从未生效的 bug,改为指数退避并补单测
Showing
2 changed files
with
116 additions
and
25 deletions
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 | 12 | import cn.hutool.core.bean.BeanUtil; |
| 13 | 13 | import cn.hutool.core.collection.CollUtil; |
| 14 | 14 | import cn.hutool.core.lang.Assert; |
| 15 | +import cn.hutool.core.thread.ThreadUtil; | |
| 15 | 16 | import cn.hutool.core.util.DesensitizedUtil; |
| 16 | 17 | import cn.hutool.core.util.ObjUtil; |
| 17 | 18 | import cn.hutool.core.util.ReflectUtil; |
| ... | ... | @@ -103,13 +104,9 @@ public class SocialClientServiceImpl implements SocialClientService { |
| 103 | 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 | 380 | .build(); |
| 384 | 381 | // 重试机制:解决支付回调与订单信息上传之间的时间差导致的 10060001 错误 |
| 385 | 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 | 386 | try { |
| 388 | 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 | 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 | 398 | log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({})]", request, ex); |
| 406 | 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 | 1 | package cn.iocoder.yudao.module.system.service.social; |
| 2 | 2 | |
| 3 | +import cn.binarywang.wx.miniapp.api.WxMaOrderShippingService; | |
| 3 | 4 | import cn.binarywang.wx.miniapp.api.WxMaService; |
| 4 | 5 | import cn.binarywang.wx.miniapp.api.WxMaUserService; |
| 5 | 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 | 9 | import cn.hutool.core.util.ReflectUtil; |
| 7 | 10 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 8 | 11 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| 9 | 12 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 10 | 13 | import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; |
| 14 | +import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO; | |
| 11 | 15 | import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; |
| 12 | 16 | import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; |
| 13 | 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 | 22 | import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; |
| 19 | 23 | import jakarta.annotation.Resource; |
| 20 | 24 | import me.chanjar.weixin.common.bean.WxJsapiSignature; |
| 25 | +import me.chanjar.weixin.common.error.WxError; | |
| 21 | 26 | import me.chanjar.weixin.common.error.WxErrorException; |
| 22 | 27 | import me.chanjar.weixin.mp.api.WxMpService; |
| 23 | 28 | import me.zhyd.oauth.config.AuthConfig; |
| ... | ... | @@ -26,6 +31,7 @@ import me.zhyd.oauth.model.AuthUser; |
| 26 | 31 | import me.zhyd.oauth.request.AuthDefaultRequest; |
| 27 | 32 | import me.zhyd.oauth.request.AuthRequest; |
| 28 | 33 | import me.zhyd.oauth.utils.AuthStateUtils; |
| 34 | +import org.junit.jupiter.api.BeforeAll; | |
| 29 | 35 | import org.junit.jupiter.api.Test; |
| 30 | 36 | import org.mockito.MockedStatic; |
| 31 | 37 | import org.springframework.context.annotation.Import; |
| ... | ... | @@ -468,4 +474,100 @@ public class SocialClientServiceImplTest extends BaseDbUnitTest { |
| 468 | 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 | } | ... | ... |