From 3e5e60ce969892f4d19d7c8593f63ac2c9d7bc6a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 3 May 2026 22:45:50 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=88system?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/tenant/TenantController.java | 2 +- .../app/tenant/AppTenantController.java | 2 +- .../permission/PermissionServiceImpl.java | 11 +- .../social/SocialClientServiceImpl.java | 39 +++---- .../permission/PermissionServiceTest.java | 55 ++++++++++ .../social/SocialClientServiceImplTest.java | 102 ++++++++++++++++++ 6 files changed, 181 insertions(+), 30 deletions(-) diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java index fba522ae6..5ed33c7d2 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java @@ -67,7 +67,7 @@ public class TenantController { @Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") public CommonResult getTenantByWebsite( - @RequestParam("website") @Pattern(regexp = "^[a-zA-Z0-9.-]+$", message = "网站域名格式不正确") String website) { + @RequestParam("website") @Pattern(regexp = "^[a-zA-Z0-9.-]+(:\\d{1,5})?$", message = "网站域名格式不正确") String website) { TenantDO tenant = tenantService.getTenantByWebsite(website); if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { return success(null); diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java index 022037d87..d8ee5bac2 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/app/tenant/AppTenantController.java @@ -36,7 +36,7 @@ public class AppTenantController { @Operation(summary = "使用域名,获得租户信息", description = "根据用户的域名,获得租户信息") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") public CommonResult getTenantByWebsite( - @RequestParam("website") @Pattern(regexp = "^[a-zA-Z0-9.-]+$", message = "网站域名格式不正确") String website) { + @RequestParam("website") @Pattern(regexp = "^[a-zA-Z0-9.-]+(:\\d{1,5})?$", message = "网站域名格式不正确") String website) { TenantDO tenant = tenantService.getTenantByWebsite(website); if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { return success(null); diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java index 8eb5b0968..d88a92130 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java @@ -303,7 +303,7 @@ public class PermissionServiceImpl implements PermissionService { CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 - CollUtil.addAll(result.getDeptIds(), userDeptId.get()); + CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get()); continue; } // 情况三,DEPT_ONLY @@ -313,9 +313,14 @@ public class PermissionServiceImpl implements PermissionService { } // 情况四,DEPT_DEPT_AND_CHILD if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { - CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get())); + Long deptId = userDeptId.get(); + // 用户未设置部门,直接跳过;否则 getChildDeptIdListFromCache 走缓存注解会因 null key 报错 + if (deptId == null) { + continue; + } + CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(deptId)); // 添加本身部门编号 - CollUtil.addAll(result.getDeptIds(), userDeptId.get()); + result.getDeptIds().add(deptId); continue; } // 情况五,SELF diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java index 0a8e30d71..51b23b11f 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/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/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java index 52ba3f938..87f12b714 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java @@ -440,6 +440,34 @@ public class PermissionServiceTest extends BaseDbUnitTest { } } + @Test + public void testGetDeptDataPermission_DeptCustom_userDeptIdNull() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_CUSTOM.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); + // mock 部门的返回:用户未设置部门 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO()); // deptId 为 null + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言:角色配置的可见部门仍正常加入,但 null 不进集合 + assertFalse(result.getAll()); + assertFalse(result.getSelf()); + assertEquals(roleDO.getDataScopeDeptIds().size(), result.getDeptIds().size()); + assertTrue(CollUtil.containsAll(result.getDeptIds(), roleDO.getDataScopeDeptIds())); + assertFalse(result.getDeptIds().contains(null)); + } + } + @Test public void testGetDeptDataPermission_DeptOnly() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { @@ -500,6 +528,33 @@ public class PermissionServiceTest extends BaseDbUnitTest { } } + @Test + public void testGetDeptDataPermission_DeptAndChild_userDeptIdNull() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_AND_CHILD.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); + // mock 部门的返回:用户未设置部门 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO()); // deptId 为 null + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言:deptId 为 null,整段跳过;deptIds 为空,子部门查询不被触发 + assertFalse(result.getAll()); + assertFalse(result.getSelf()); + assertTrue(CollUtil.isEmpty(result.getDeptIds())); + verify(deptService, never()).getChildDeptIdListFromCache(any()); + } + } + @Test public void testGetDeptDataPermission_Self() { try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java index ae87de0fa..a4c85f7f8 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/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()); + } + }