From 431f73469e37918ebf51e1fd69616a4f1b0b59e9 Mon Sep 17 00:00:00 2001 From: hy001 Date: Fri, 10 Jul 2026 17:42:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=AE=BE=E5=A4=87=E6=8C=87=E7=BA=B9?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E9=99=90=E5=88=B6=E7=9A=84=E5=B0=81=E7=A6=81?= =?UTF-8?q?=E8=B1=81=E5=85=8D=E8=A1=A5=E5=88=B0=E7=9C=9F=E6=AD=A3=E7=9A=84?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E9=93=BE=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上次修复(0a29464)只改了 other-inner-endpoint 的 RegisterDeviceClientServiceImpl, 但全部 8 个业务拦截点(BD邀请代理/接受邀请建团/代理邀请主播/申请加入公会/后台加成员等) 走的都是 other-infrastructure 的 RegisterDeviceGatewayImpl(领域网关唯一实现), 该实现没有封禁豁免,导致线上 BD 邀请仍报 6054。 本次修复: 1. RegisterDeviceGatewayImpl 循环内补豁免:同设备账号已封禁(checkAccountArchive) 或已注销(profile 为 null,逻辑删过滤)的,不再占用设备主播/代理名额; 2. 设备白名单改为按分隔符切分后精确比对账号,修复子串误命中(账号123被白名单51234命中); 3. RegisterDeviceClientServiceImpl 的重复实现改为单行委托领域网关,消除双份逻辑漂移; 4. 新增 RegisterDeviceGatewayImplTest 覆盖豁免/白名单/手动黑白名单 7 个场景。 Co-Authored-By: Claude Fable 5 --- .../user/RegisterDeviceGatewayImpl.java | 32 ++++- .../user/RegisterDeviceGatewayImplTest.java | 136 ++++++++++++++++++ .../impl/RegisterDeviceClientServiceImpl.java | 60 +------- 3 files changed, 168 insertions(+), 60 deletions(-) create mode 100644 rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/gateway/user/RegisterDeviceGatewayImplTest.java diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/user/RegisterDeviceGatewayImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/user/RegisterDeviceGatewayImpl.java index 23db3c30..b45225fd 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/user/RegisterDeviceGatewayImpl.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/user/RegisterDeviceGatewayImpl.java @@ -1,6 +1,7 @@ package com.red.circle.other.infra.gateway.user; 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.cache.service.other.EnumConfigCacheService; import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService; @@ -99,7 +100,7 @@ public class RegisterDeviceGatewayImpl implements RegisterDeviceGateway { return "BLOCK".equals(status); } - com.red.circle.other.domain.model.user.UserProfile userProfile = userProfileGateway.getByUserId(userId); + UserProfile userProfile = userProfileGateway.getByUserId(userId); if (userProfile == null) { return false; } @@ -112,7 +113,7 @@ public class RegisterDeviceGatewayImpl implements RegisterDeviceGateway { // 跳过白名单用户 String whitelistConfig = enumConfigCacheService.getValue( EnumConfigKey.DEVICE_REGISTER_WHITELIST, userProfile.getOriginSys()); - if (StringUtils.isNotBlank(whitelistConfig) && whitelistConfig.contains(String.valueOf(userProfile.getAccount()))) { + if (matchDeviceWhitelist(whitelistConfig, String.valueOf(userProfile.getAccount()))) { return false; } @@ -131,11 +132,34 @@ public class RegisterDeviceGatewayImpl implements RegisterDeviceGateway { // 判断是否为主播:检查用户是否为团队成员 TeamMember teamMember = teamMemberService.getByMemberId(otherUserId); - if (teamMember != null) { + if (teamMember == null) { + continue; + } + + // 同设备账号已被封禁(checkAccountArchive)或已注销(profile 为 null)的,不再占用该设备的主播/代理名额 + UserProfile otherProfile = userProfileGateway.getByUserId(otherUserId); + if (otherProfile == null || otherProfile.checkAccountArchive()) { + continue; + } + + return true; + } + + return false; + } + + /** + * 设备白名单按分隔符切分后精确比对账号,避免子串误命中(如账号 123 被白名单 51234 命中). + */ + private boolean matchDeviceWhitelist(String whitelistConfig, String account) { + if (StringUtils.isBlank(whitelistConfig) || StringUtils.isBlank(account)) { + return false; + } + for (String item : whitelistConfig.split("[,,;;\\s]+")) { + if (account.equals(item.trim())) { return true; } } - return false; } diff --git a/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/gateway/user/RegisterDeviceGatewayImplTest.java b/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/gateway/user/RegisterDeviceGatewayImplTest.java new file mode 100644 index 00000000..4ed35df8 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/gateway/user/RegisterDeviceGatewayImplTest.java @@ -0,0 +1,136 @@ +package com.red.circle.other.infra.gateway.user; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.red.circle.common.business.enums.AccountStatusEnum; +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.cache.service.other.EnumConfigCacheService; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; +import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService; +import com.red.circle.other.infra.database.rds.service.user.device.DeviceRegisterQuantityService; +import com.red.circle.other.infra.database.rds.service.user.device.IpRegisterQuantityService; +import com.red.circle.other.infra.database.rds.service.user.device.LatestMobileDeviceService; +import com.red.circle.other.infra.database.rds.service.user.user.ExpandService; +import com.red.circle.tool.core.date.TimestampUtils; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class RegisterDeviceGatewayImplTest { + + private static final Long SELF_ID = 100L; + private static final Long OTHER_ID = 200L; + private static final String FINGERPRINT = "Android|Infinix X6871|1.2.3.4"; + + private IpRegisterQuantityService ipRegisterQuantityService; + private DeviceRegisterQuantityService deviceRegisterQuantityService; + private LatestMobileDeviceService latestMobileDeviceService; + private UserProfileGateway userProfileGateway; + private TeamMemberService teamMemberService; + private EnumConfigCacheService enumConfigCacheService; + private ExpandService expandService; + private RegisterDeviceGatewayImpl gateway; + + @BeforeEach + void setUp() { + ipRegisterQuantityService = mock(IpRegisterQuantityService.class); + deviceRegisterQuantityService = mock(DeviceRegisterQuantityService.class); + latestMobileDeviceService = mock(LatestMobileDeviceService.class); + userProfileGateway = mock(UserProfileGateway.class); + teamMemberService = mock(TeamMemberService.class); + enumConfigCacheService = mock(EnumConfigCacheService.class); + expandService = mock(ExpandService.class); + gateway = new RegisterDeviceGatewayImpl( + ipRegisterQuantityService, + deviceRegisterQuantityService, + latestMobileDeviceService, + userProfileGateway, + teamMemberService, + enumConfigCacheService, + expandService); + + when(expandService.getDeviceListBlock(anyLong())).thenReturn(null); + when(userProfileGateway.getByUserId(SELF_ID)).thenReturn(profile(SELF_ID, "1001", AccountStatusEnum.NORMAL, 0)); + when(latestMobileDeviceService.getDeviceFingerprintAndIPByUserId(SELF_ID)).thenReturn(FINGERPRINT); + when(latestMobileDeviceService.listUserIdsByDeviceFingerprint(FINGERPRINT, "LIKEI")) + .thenReturn(List.of(SELF_ID, OTHER_ID)); + when(teamMemberService.getByMemberId(OTHER_ID)).thenReturn(new TeamMember().setMemberId(OTHER_ID)); + } + + private UserProfile profile(Long id, String account, AccountStatusEnum status, int freezeDays) { + UserProfile profile = new UserProfile(); + profile.setId(id); + profile.setAccount(account); + profile.setOriginSys("LIKEI"); + profile.setAccountStatus(status.name()); + profile.setFreezingTime(freezeDays == 0 ? TimestampUtils.now() : TimestampUtils.nowPlusDays(freezeDays)); + return profile; + } + + @Test + void blocksWhenSameDeviceHasActiveTeamMember() { + when(userProfileGateway.getByUserId(OTHER_ID)).thenReturn(profile(OTHER_ID, "1002", AccountStatusEnum.NORMAL, 0)); + + assertTrue(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + } + + @Test + void exemptsArchivedAccountOnSameDevice() { + when(userProfileGateway.getByUserId(OTHER_ID)).thenReturn(profile(OTHER_ID, "1002", AccountStatusEnum.ARCHIVE, 365)); + + assertFalse(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + } + + @Test + void exemptsArchiveDeviceAccountOnSameDevice() { + when(userProfileGateway.getByUserId(OTHER_ID)).thenReturn(profile(OTHER_ID, "1002", AccountStatusEnum.ARCHIVE_DEVICE, 0)); + + assertFalse(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + } + + @Test + void exemptsDeletedAccountOnSameDevice() { + // 已注销账号 getByUserId 返回 null(逻辑删过滤),不应永久占用设备名额 + when(userProfileGateway.getByUserId(OTHER_ID)).thenReturn(null); + + assertFalse(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + } + + @Test + void expiredArchiveStillBlocks() { + // ARCHIVE 但冻结期已过 = 已自动解封,重新占用名额 + UserProfile expired = profile(OTHER_ID, "1002", AccountStatusEnum.ARCHIVE, 0); + when(userProfileGateway.getByUserId(OTHER_ID)).thenReturn(expired); + + assertTrue(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + } + + @Test + void whitelistMatchesExactAccountOnly() { + when(userProfileGateway.getByUserId(OTHER_ID)).thenReturn(profile(OTHER_ID, "1002", AccountStatusEnum.NORMAL, 0)); + + // 子串不再误命中:白名单 51001 不应豁免账号 1001 + when(enumConfigCacheService.getValue(com.red.circle.other.inner.enums.config.EnumConfigKey.DEVICE_REGISTER_WHITELIST, "LIKEI")) + .thenReturn("51001,9999"); + assertTrue(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + + // 精确命中豁免 + when(enumConfigCacheService.getValue(com.red.circle.other.inner.enums.config.EnumConfigKey.DEVICE_REGISTER_WHITELIST, "LIKEI")) + .thenReturn("51001,1001"); + assertFalse(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + } + + @Test + void manualDeviceListOverridesFingerprintLogic() { + when(expandService.getDeviceListBlock(SELF_ID)).thenReturn(Boolean.TRUE); + assertTrue(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + + when(expandService.getDeviceListBlock(SELF_ID)).thenReturn(Boolean.FALSE); + assertFalse(gateway.checkDeviceFingerprintHasAnchor(SELF_ID)); + } +} diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/user/device/impl/RegisterDeviceClientServiceImpl.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/user/device/impl/RegisterDeviceClientServiceImpl.java index 9e645b31..c3ecb020 100644 --- a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/user/device/impl/RegisterDeviceClientServiceImpl.java +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/user/device/impl/RegisterDeviceClientServiceImpl.java @@ -7,17 +7,14 @@ import com.red.circle.other.app.inner.convertor.user.RegisterDeviceInnerConverto import com.red.circle.other.app.inner.convertor.user.UserProfileInnerConvertor; import com.red.circle.other.app.inner.service.user.device.RegisterDeviceClientService; import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.gateway.user.ability.RegisterDeviceGateway; import com.red.circle.other.domain.model.user.UserProfile; -import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService; -import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; -import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService; import com.red.circle.other.infra.database.rds.entity.user.user.LatestMobileDevice; import com.red.circle.other.infra.database.rds.service.user.device.ArchiveDeviceService; import com.red.circle.other.infra.database.rds.service.user.device.DeviceRegisterQuantityService; import com.red.circle.other.infra.database.rds.service.user.device.LatestMobileDeviceService; import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.text.StringUtils; -import com.red.circle.other.inner.enums.config.EnumConfigKey; import com.red.circle.other.inner.model.cmd.user.device.ArchiveDeviceQryCmd; import com.red.circle.other.inner.model.cmd.user.device.DeviceRegisterQuantityQryCmd; import com.red.circle.other.inner.model.cmd.user.device.PageLatestMobileDeviceQryCmd; @@ -53,8 +50,7 @@ public class RegisterDeviceClientServiceImpl implements RegisterDeviceClientServ private final LatestMobileDeviceService latestMobileDeviceService; private final RegisterDeviceInnerConvertor registerDeviceInnerConvertor; private final DeviceRegisterQuantityService deviceRegisterQuantityService; - private final TeamMemberService teamMemberService; - private final EnumConfigCacheService enumConfigCacheService; + private final RegisterDeviceGateway registerDeviceGateway; @Override public Boolean existsDevice(String deviceNo, String sysOrigin) { @@ -243,55 +239,7 @@ public class RegisterDeviceClientServiceImpl implements RegisterDeviceClientServ @Override public Boolean checkDeviceFingerprintHasAnchor(Long userId) { - if (userId == null) { - return false; - } - - UserProfile userProfile = userProfileGateway.getByUserId(userId); - if (userProfile == null) { - return false; - } - - String fingerprint = latestMobileDeviceService.getDeviceFingerprintAndIPByUserId(userId); - if (StringUtils.isBlank(fingerprint)) { - return false; - } - - // 跳过白名单用户 - String whitelistConfig = enumConfigCacheService.getValue( - EnumConfigKey.DEVICE_REGISTER_WHITELIST, userProfile.getOriginSys()); - if (StringUtils.isNotBlank(whitelistConfig) && whitelistConfig.contains(String.valueOf(userProfile.getAccount()))) { - return false; - } - - List userIds = latestMobileDeviceService.listUserIdsByDeviceFingerprint( - fingerprint, userProfile.getOriginSys()); - - if (CollectionUtils.isEmpty(userIds)) { - return false; - } - - for (Long otherUserId : userIds) { - if (Objects.equals(otherUserId, userId)) { - continue; - } - - // 判断是否为主播:检查用户是否为团队成员 - TeamMember teamMember = - teamMemberService.getByMemberId(otherUserId); - if (teamMember == null) { - continue; - } - - // 同设备账号已被封禁的,不再占用该设备的主播/代理名额 - UserProfile otherProfile = userProfileGateway.getByUserId(otherUserId); - if (otherProfile != null && otherProfile.checkAccountArchive()) { - continue; - } - - return true; - } - - return false; + // 统一委托领域网关实现,避免两份指纹校验逻辑漂移(封禁/注销豁免、白名单、手动黑白名单前置均以网关为准)。 + return registerDeviceGateway.checkDeviceFingerprintHasAnchor(userId); } }