【同步】BOOT 和 CLOUD 的功能(system)

pull/248/MERGE
YunaiV 2026-05-03 22:45:50 +08:00
parent f57f0c551c
commit 3e5e60ce96
6 changed files with 181 additions and 30 deletions

View File

@ -67,7 +67,7 @@ public class TenantController {
@Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") @Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息")
@Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn")
public CommonResult<TenantRespVO> getTenantByWebsite( public CommonResult<TenantRespVO> 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); TenantDO tenant = tenantService.getTenantByWebsite(website);
if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) {
return success(null); return success(null);

View File

@ -36,7 +36,7 @@ public class AppTenantController {
@Operation(summary = "使用域名,获得租户信息", description = "根据用户的域名,获得租户信息") @Operation(summary = "使用域名,获得租户信息", description = "根据用户的域名,获得租户信息")
@Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn")
public CommonResult<AppTenantRespVO> getTenantByWebsite( public CommonResult<AppTenantRespVO> 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); TenantDO tenant = tenantService.getTenantByWebsite(website);
if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) {
return success(null); return success(null);

View File

@ -303,7 +303,7 @@ public class PermissionServiceImpl implements PermissionService {
CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds());
// 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。
// 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉
CollUtil.addAll(result.getDeptIds(), userDeptId.get()); CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get());
continue; continue;
} }
// 情况三DEPT_ONLY // 情况三DEPT_ONLY
@ -313,9 +313,14 @@ public class PermissionServiceImpl implements PermissionService {
} }
// 情况四DEPT_DEPT_AND_CHILD // 情况四DEPT_DEPT_AND_CHILD
if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { 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; continue;
} }
// 情况五SELF // 情况五SELF

View File

@ -12,6 +12,7 @@ import cn.binarywang.wx.miniapp.constant.WxMaConstants;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
@ -103,13 +104,9 @@ public class SocialClientServiceImpl implements SocialClientService {
public String miniprogramState; public String miniprogramState;
/** /**
* * 1 + 2 + 4 = 7
*/ */
private static final int UPLOAD_SHIPPING_INFO_MAX_RETRIES = 5; private static final long[] UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS = {1000, 2000, 4000};
/**
*
*/
private static final Duration UPLOAD_SHIPPING_INFO_RETRY_INTERVAL = Duration.ofMillis(500L);
/** /**
* *
*/ */
@ -383,31 +380,23 @@ public class SocialClientServiceImpl implements SocialClientService {
.build(); .build();
// 重试机制:解决支付回调与订单信息上传之间的时间差导致的 10060001 错误 // 重试机制:解决支付回调与订单信息上传之间的时间差导致的 10060001 错误
// 对应 ISSUEhttps://gitee.com/zhijiantianya/yudao-cloud/pulls/230 // 对应 ISSUEhttps://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 { try {
WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request); WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request);
// 成功,直接返回
if (response.getErrCode() == 0) {
log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功request({}) response({})]", request, response); log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功request({}) response({})]", request, response);
return; return;
} } catch (WxErrorException ex) {
// 如果是 10060001 错误(支付单不存在)且还有重试次数,则等待后重试 if (ex.getError().getErrorCode() == WX_ERR_CODE_PAY_ORDER_NOT_EXIST && attempt < maxAttempts) {
if (response.getErrCode() == WX_ERR_CODE_PAY_ORDER_NOT_EXIST && attempt < UPLOAD_SHIPPING_INFO_MAX_RETRIES) { long delayMillis = UPLOAD_SHIPPING_INFO_RETRY_BACKOFF_MILLIS[attempt - 1];
log.warn("[uploadWxaOrderShippingInfo][第 {} 次尝试失败,支付单不存在,{} 后重试request({}) response({})]", log.warn("[uploadWxaOrderShippingInfo][第 {} 次尝试失败,支付单不存在,{} ms 后重试request({})]",
attempt, UPLOAD_SHIPPING_INFO_RETRY_INTERVAL, request, response); attempt, delayMillis, request, ex);
Thread.sleep(UPLOAD_SHIPPING_INFO_RETRY_INTERVAL.toMillis()); ThreadUtil.sleep(delayMillis);
continue; 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); log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败request({})]", request, ex);
throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, ex.getError().getErrorMsg()); 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, "重试等待被中断");
} }
} }
} }

View File

@ -440,6 +440,34 @@ public class PermissionServiceTest extends BaseDbUnitTest {
} }
} }
@Test
public void testGetDeptDataPermission_DeptCustom_userDeptIdNull() {
try (MockedStatic<SpringUtil> 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 @Test
public void testGetDeptDataPermission_DeptOnly() { public void testGetDeptDataPermission_DeptOnly() {
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
@ -500,6 +528,33 @@ public class PermissionServiceTest extends BaseDbUnitTest {
} }
} }
@Test
public void testGetDeptDataPermission_DeptAndChild_userDeptIdNull() {
try (MockedStatic<SpringUtil> 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 @Test
public void testGetDeptDataPermission_Self() { public void testGetDeptDataPermission_Self() {
try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {

View File

@ -1,13 +1,17 @@
package cn.iocoder.yudao.module.system.service.social; 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.WxMaService;
import cn.binarywang.wx.miniapp.api.WxMaUserService; import cn.binarywang.wx.miniapp.api.WxMaUserService;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; 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.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 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.SocialClientPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO; 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 com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import me.chanjar.weixin.common.bean.WxJsapiSignature; 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.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.WxMpService;
import me.zhyd.oauth.config.AuthConfig; 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.AuthDefaultRequest;
import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils; import me.zhyd.oauth.utils.AuthStateUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -468,4 +474,100 @@ public class SocialClientServiceImplTest extends BaseDbUnitTest {
assertPojoEquals(dbSocialClient, pageResult.getList().get(0)); 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());
}
} }