From 2ef0ac87d50ff52c7f7cd140dae774092132b992 Mon Sep 17 00:00:00 2001 From: zhx Date: Sat, 27 Jun 2026 17:42:06 +0800 Subject: [PATCH] fix: adjust admin invite country guard --- .../app/command/team/AgentInviteHostExe.java | 5 ++ .../app/command/team/BdAgentInviteExe.java | 18 ++-- .../app/command/team/BdLeaderInviteBdExe.java | 17 ++-- .../app/command/team/TeamInviteGuard.java | 75 +++++++++++++++++ .../app/command/team/TeamInviteGuardTest.java | 82 +++++++++++++++++++ 5 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/TeamInviteGuard.java create mode 100644 rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/team/TeamInviteGuardTest.java diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/AgentInviteHostExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/AgentInviteHostExe.java index a61543f2..6057fc2e 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/AgentInviteHostExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/AgentInviteHostExe.java @@ -55,12 +55,17 @@ public class AgentInviteHostExe { private final RegisterDeviceGateway registerDeviceGateway; private final UserProfileAppConvertor userProfileAppConvertor; private final BdInviteAgentMessageService bdInviteAgentMessageService; + private final TeamInviteGuard teamInviteGuard; public void execute(AgentInviteHostCmd cmd) { // 检查权限 boolean teamOwn = teamProfileService.existsOwnUserId(cmd.getReqUserId()); ResponseAssert.isTrue(TeamErrorCode.USER_AGENT_ROLE_INVALID_ERROR, teamOwn); + // 只有APP管理员发起邀请时才强制同国家;非管理员保留原邀请规则. + teamInviteGuard.assertSameCountryWhenAdminInviter(cmd.requiredReqUserId(), + cmd.getInviteUserId()); + UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO( userProfileGateway.getByUserId(cmd.getInviteUserId())); ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, userProfile); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/BdAgentInviteExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/BdAgentInviteExe.java index 79acda3d..b9040549 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/BdAgentInviteExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/BdAgentInviteExe.java @@ -19,7 +19,6 @@ import com.red.circle.other.infra.database.mongo.service.team.bd.BdInviteAgentMe import com.red.circle.other.infra.database.mongo.service.team.team.TeamApplicationProcessService; import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService; import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileService; -import com.red.circle.other.infra.database.rds.service.sys.AdministratorService; import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentBaseInfoService; import com.red.circle.other.infra.database.rds.service.user.device.LatestMobileDeviceService; import com.red.circle.other.inner.asserts.user.UserErrorCode; @@ -62,17 +61,21 @@ public class BdAgentInviteExe { private final BdInviteAgentMessageService bdInviteAgentMessageService; private final TeamApplicationProcessService teamApplicationProcessService; private final BusinessDevelopmentBaseInfoService businessDevelopmentBaseInfoService; - private final AdministratorService administratorService; + private final TeamInviteGuard teamInviteGuard; public void execute(DbInviteAgentCmd cmd) { - ResponseAssert.isFalse(TeamErrorCode.USER_IS_A_ADMIN, - administratorService.existsAdmin(cmd.getInviteUserId())); + teamInviteGuard.assertInviteeIsNotAdminUnlessSelf(cmd.requiredReqUserId(), + cmd.getInviteUserId()); // 权限不足 ResponseAssert.isTrue(CommonErrorCode.INSUFFICIENT_PERMISSION, businessDevelopmentBaseInfoService.checkBD(cmd.requiredReqUserId())); + // APP管理员发起邀请时只能邀请同国家用户;普通BD继续沿用原有区域和余额规则. + boolean adminCountryChecked = teamInviteGuard.assertSameCountryWhenAdminInviter( + cmd.requiredReqUserId(), cmd.getInviteUserId()); + // 用户已经是主播 ResponseAssert.isFalse(TeamErrorCode.USER_IS_ALREADY_A_ANCHOR, teamMemberService.existsMember(cmd.getInviteUserId())); @@ -94,8 +97,9 @@ public class BdAgentInviteExe { bdInviteAgentMessageService.existsWaitMessage(cmd.requiredReqUserId(), cmd.getInviteUserId(), MessageInviteGroup.BD_INVITE_TEAM.name())); - // 校验邀请人与被邀请人的区域 - checkInvite(cmd); + if (!adminCountryChecked) { + checkInvite(cmd); + } String messageId = IdWorkerUtils.getIdStr(); bdInviteAgentMessageService.add(new BdInviteAgentMessage() @@ -139,7 +143,7 @@ public class BdAgentInviteExe { private void checkInvite(DbInviteAgentCmd cmd) { - // 验证BD区域(如BD是代理则按照代理的区域来否则用BD本身指定的区域)与被邀请人区域是不是一致, + // 普通BD保持旧规则:同区域直接放行;不同区域但被邀请人余额低于100也放行. Boolean eqRegion = userRegionGateway.checkEqRegion(cmd.requiredReqUserId(), cmd.getInviteUserId()); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/BdLeaderInviteBdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/BdLeaderInviteBdExe.java index 89d7868c..32373444 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/BdLeaderInviteBdExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/BdLeaderInviteBdExe.java @@ -15,7 +15,6 @@ import com.red.circle.other.infra.database.mongo.entity.team.bd.BdInviteAgentMes import com.red.circle.other.infra.database.mongo.entity.team.bd.MessageInviteGroup; import com.red.circle.other.infra.database.mongo.service.team.bd.BdInviteAgentMessageService; import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentBaseInfo; -import com.red.circle.other.infra.database.rds.service.sys.AdministratorService; import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentBaseInfoService; import com.red.circle.other.infra.database.rds.service.team.RoomBdLeadService; import com.red.circle.other.inner.asserts.user.UserErrorCode; @@ -50,7 +49,7 @@ public class BdLeaderInviteBdExe { private final UserProfileAppConvertor userProfileAppConvertor; private final BdInviteAgentMessageService bdInviteAgentMessageService; private final BusinessDevelopmentBaseInfoService businessDevelopmentBaseInfoService; - private final AdministratorService administratorService; + private final TeamInviteGuard teamInviteGuard; public void execute(DbLeaderInviteBdCmd cmd) { @@ -65,9 +64,12 @@ public class BdLeaderInviteBdExe { ResponseAssert.isFalse(TeamErrorCode.USER_IS_ALREADY_A_BD, businessDevelopmentBaseInfoService.checkBD(cmd.getInviteUserId())); - ResponseAssert.isFalse(TeamErrorCode.USER_IS_A_ADMIN, - administratorService.existsAdmin(cmd.getInviteUserId())); + teamInviteGuard.assertInviteeIsNotAdminUnlessSelf(cmd.requiredReqUserId(), + cmd.getInviteUserId()); + // 只有APP管理员发起邀请时才强制同国家;非管理员保留原邀请规则. + teamInviteGuard.assertSameCountryWhenAdminInviter(cmd.requiredReqUserId(), + cmd.getInviteUserId()); // 权限不足 BusinessDevelopmentBaseInfo bd = getDevelopmentBaseInfoServiceByUserId(cmd); @@ -117,9 +119,12 @@ public class BdLeaderInviteBdExe { ResponseAssert.isFalse(TeamErrorCode.USER_IS_ALREADY_A_BDLEADER, roomBdLeadService.checkBdLeader(cmd.getInviteUserId())); - ResponseAssert.isFalse(TeamErrorCode.USER_IS_A_ADMIN, - administratorService.existsAdmin(cmd.getInviteUserId())); + teamInviteGuard.assertInviteeIsNotAdminUnlessSelf(cmd.requiredReqUserId(), + cmd.getInviteUserId()); + // 只有APP管理员发起邀请时才强制同国家;非管理员保留原邀请规则. + teamInviteGuard.assertSameCountryWhenAdminInviter(cmd.requiredReqUserId(), + cmd.getInviteUserId()); List bdUserIds = businessDevelopmentBaseInfoService.listBdUserIdsByLeaderId(cmd.requiredReqUserId()); ResponseAssert.isFalse(CommonErrorCode.INSUFFICIENT_PERMISSION, bdUserIds.contains(cmd.getInviteUserId())); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/TeamInviteGuard.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/TeamInviteGuard.java new file mode 100644 index 00000000..4d8f7e3a --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/team/TeamInviteGuard.java @@ -0,0 +1,75 @@ +package com.red.circle.other.app.command.team; + +import com.red.circle.framework.core.asserts.ResponseAssert; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.model.user.UserProfile; +import com.red.circle.other.infra.database.rds.service.sys.AdministratorService; +import com.red.circle.other.inner.asserts.team.TeamErrorCode; +import com.red.circle.other.inner.asserts.user.UserErrorCode; +import com.red.circle.tool.core.text.StringUtils; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 团队邀请的公共身份和国家校验. + */ +@Component +@RequiredArgsConstructor +public class TeamInviteGuard { + + private static final Map COUNTRY_CODE_ALIASES = Map.of( + "IA", "ID", + "EY", "EG", + "AB", "AE", + "MB", "MA", + "SP", "SA" + ); + + private final AdministratorService administratorService; + private final UserProfileGateway userProfileGateway; + + public void assertInviteeIsNotAdminUnlessSelf(Long reqUserId, Long inviteUserId) { + if (Objects.equals(reqUserId, inviteUserId)) { + return; + } + ResponseAssert.isFalse(TeamErrorCode.USER_IS_A_ADMIN, + administratorService.existsAdmin(inviteUserId)); + } + + public boolean assertSameCountryWhenAdminInviter(Long reqUserId, Long inviteUserId) { + if (!isAdminInviteScope(reqUserId, inviteUserId)) { + return false; + } + UserProfile reqUser = userProfileGateway.getByUserId(reqUserId); + UserProfile inviteUser = userProfileGateway.getByUserId(inviteUserId); + ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, reqUser); + ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, inviteUser); + ResponseAssert.isTrue(TeamErrorCode.NOT_INVITE_USER_ERROR, isSameCountry(reqUser, inviteUser)); + return true; + } + + public boolean isAdminInviteScope(Long reqUserId, Long inviteUserId) { + return !Objects.equals(reqUserId, inviteUserId) && administratorService.existsAdmin(reqUserId); + } + + private boolean isSameCountry(UserProfile reqUser, UserProfile inviteUser) { + String reqCountryCode = normalizeCountryCode(reqUser.getCountryCode()); + String inviteCountryCode = normalizeCountryCode(inviteUser.getCountryCode()); + if (StringUtils.isNotBlank(reqCountryCode) && StringUtils.isNotBlank(inviteCountryCode)) { + return Objects.equals(reqCountryCode, inviteCountryCode); + } + return Objects.nonNull(reqUser.getCountryId()) + && Objects.equals(reqUser.getCountryId(), inviteUser.getCountryId()); + } + + private String normalizeCountryCode(String countryCode) { + if (StringUtils.isBlank(countryCode)) { + return null; + } + String normalized = countryCode.trim().toUpperCase(Locale.ROOT); + return COUNTRY_CODE_ALIASES.getOrDefault(normalized, normalized); + } +} diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/team/TeamInviteGuardTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/team/TeamInviteGuardTest.java new file mode 100644 index 00000000..7fed2913 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/team/TeamInviteGuardTest.java @@ -0,0 +1,82 @@ +package com.red.circle.other.app.command.team; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.model.user.UserProfile; +import com.red.circle.other.infra.database.rds.service.sys.AdministratorService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TeamInviteGuardTest { + + @Mock + private AdministratorService administratorService; + @Mock + private UserProfileGateway userProfileGateway; + + private TeamInviteGuard teamInviteGuard; + + @BeforeEach + void setUp() { + teamInviteGuard = new TeamInviteGuard(administratorService, userProfileGateway); + } + + @Test + void shouldNotBlockAdminWhenInviteSelf() { + assertDoesNotThrow(() -> teamInviteGuard.assertInviteeIsNotAdminUnlessSelf(1L, 1L)); + + verify(administratorService, never()).existsAdmin(1L); + } + + @Test + void shouldBlockAdminWhenInviteOtherUser() { + when(administratorService.existsAdmin(2L)).thenReturn(true); + + assertThrows(RuntimeException.class, + () -> teamInviteGuard.assertInviteeIsNotAdminUnlessSelf(1L, 2L)); + } + + @Test + void shouldAllowLegacyAliasCountryCode() { + when(administratorService.existsAdmin(1L)).thenReturn(true); + when(userProfileGateway.getByUserId(1L)).thenReturn(profile(1L, "SP")); + when(userProfileGateway.getByUserId(2L)).thenReturn(profile(2L, "SA")); + + assertDoesNotThrow(() -> teamInviteGuard.assertSameCountryWhenAdminInviter(1L, 2L)); + } + + @Test + void shouldBlockDifferentCountryForAdminInviter() { + when(administratorService.existsAdmin(1L)).thenReturn(true); + when(userProfileGateway.getByUserId(1L)).thenReturn(profile(1L, "BD")); + when(userProfileGateway.getByUserId(2L)).thenReturn(profile(2L, "PK")); + + assertThrows(RuntimeException.class, + () -> teamInviteGuard.assertSameCountryWhenAdminInviter(1L, 2L)); + } + + @Test + void shouldSkipCountryCheckForNonAdminInviter() { + when(administratorService.existsAdmin(1L)).thenReturn(false); + + assertDoesNotThrow(() -> teamInviteGuard.assertSameCountryWhenAdminInviter(1L, 2L)); + verify(userProfileGateway, never()).getByUserId(1L); + verify(userProfileGateway, never()).getByUserId(2L); + } + + private UserProfile profile(Long countryId, String countryCode) { + UserProfile profile = new UserProfile(); + profile.setCountryId(countryId); + profile.setCountryCode(countryCode); + return profile; + } +}