diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/common/team/decay/TeamMemberRemoveDecay.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/common/team/decay/TeamMemberRemoveDecay.java index 95cd83f3..2f9c2ff8 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/common/team/decay/TeamMemberRemoveDecay.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/common/team/decay/TeamMemberRemoveDecay.java @@ -28,9 +28,10 @@ import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileSe import com.red.circle.other.infra.database.rds.entity.team.TeamTerminationFeeRecord; import com.red.circle.other.infra.database.rds.service.team.BillDiamondBalanceDetailsService; import com.red.circle.other.infra.database.rds.service.team.BillDiamondBalanceService; -import com.red.circle.other.infra.database.rds.service.team.TeamTerminationFeeRecordService; -import com.red.circle.other.inner.enums.material.BadgeKeyEnum; -import com.red.circle.other.inner.model.cmd.team.TeamMemberIdCmd; +import com.red.circle.other.infra.database.rds.service.team.TeamTerminationFeeRecordService; +import com.red.circle.other.inner.enums.material.BadgeKeyEnum; +import com.red.circle.other.inner.asserts.team.TeamErrorCode; +import com.red.circle.other.inner.model.cmd.team.TeamMemberIdCmd; import com.red.circle.other.inner.enums.team.TeamAppProcessApprovalReason; import com.red.circle.other.inner.enums.team.TeamApplicationType; import com.red.circle.other.inner.enums.team.TeamMemberRole; @@ -181,12 +182,20 @@ public class TeamMemberRemoveDecay { teamMemberTargetService.getLatestInProgressTarget(teamMember.getMemberId())); } - private TeamMember processRemoveTeamMember(Long memberSortId) { - String lockKey = "REMOVE_TEAM_MEMBER:" + memberSortId; - if (!redisService.lock(lockKey, 60)) { - return null; - } - TeamMember teamMember = teamMemberService.removeMember(memberSortId); + private TeamMember processRemoveTeamMember(Long memberSortId) { + String lockKey = "REMOVE_TEAM_MEMBER:" + memberSortId; + if (!redisService.lock(lockKey, 60)) { + return null; + } + + TeamMember existingTeamMember = teamMemberService.getByMemberId(memberSortId); + if (Objects.nonNull(existingTeamMember) && TeamMemberRole.OWN.eq(existingTeamMember.getRole())) { + redisService.unlock(lockKey); + ResponseAssert.isFalse(TeamErrorCode.PERMISSION_DENIED, true); + return null; + } + + TeamMember teamMember = teamMemberService.removeMember(memberSortId); if (Objects.isNull(teamMember)) { redisService.unlock(lockKey); return null; diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamMemberServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamMemberServiceImpl.java index dbc8866d..133ffbd5 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamMemberServiceImpl.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamMemberServiceImpl.java @@ -7,13 +7,15 @@ import com.mongodb.BasicDBObject; import com.red.circle.common.business.core.enums.OpUserType; import com.red.circle.common.business.enums.OperationUserOrigin; import com.red.circle.framework.mybatis.constant.PageConstant; -import com.red.circle.other.infra.database.mongo.dto.team.TeamMemberCount; -import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; -import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMemberTarget; -import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService; -import com.red.circle.other.inner.model.cmd.team.TeamMemberBackQryCmd; -import com.red.circle.other.inner.model.dto.agency.agency.TeamRemark; -import com.red.circle.other.inner.enums.team.TeamMemberRole; +import com.red.circle.other.infra.database.mongo.dto.team.TeamMemberCount; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMemberTarget; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile; +import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService; +import com.red.circle.other.inner.model.cmd.team.TeamMemberBackQryCmd; +import com.red.circle.other.inner.model.dto.agency.agency.TeamRemark; +import com.red.circle.other.inner.enums.team.TeamMemberRole; +import com.red.circle.other.inner.model.dto.agency.agency.TeamStatus; import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.date.TimestampUtils; import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils; @@ -136,11 +138,28 @@ public class TeamMemberServiceImpl implements TeamMemberService { TeamMember.class); } - @Override - public TeamMember getByMemberId(Long memberId) { - return mongoTemplate.findOne(Query.query(Criteria.where("memberId").is(memberId)), - TeamMember.class); - } + @Override + public TeamMember getByMemberId(Long memberId) { + List teamMembers = mongoTemplate.find(Query.query(Criteria.where("memberId").is(memberId)) + .with(Sort.by(Sort.Order.desc("sortId"))), + TeamMember.class); + if (CollectionUtils.isEmpty(teamMembers)) { + return null; + } + + Set availableTeamIds = mongoTemplate.find(Query.query(Criteria.where("id") + .in(teamMembers.stream().map(TeamMember::getTeamId).collect(Collectors.toSet())) + .and("status").is(TeamStatus.AVAILABLE)), + TeamProfile.class) + .stream() + .map(TeamProfile::getId) + .collect(Collectors.toSet()); + + return teamMembers.stream() + .filter(teamMember -> availableTeamIds.contains(teamMember.getTeamId())) + .findFirst() + .orElse(teamMembers.get(0)); + } @Override public TeamMember getTeamOwn(Long teamId) { diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamProfileServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamProfileServiceImpl.java index 9e6947fd..7e005bf9 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamProfileServiceImpl.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamProfileServiceImpl.java @@ -4,10 +4,12 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.mongodb.BasicDBObject; -import com.red.circle.common.business.core.enums.OpUserType; -import com.red.circle.framework.mybatis.constant.PageConstant; -import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile; -import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileService; +import com.red.circle.common.business.core.enums.OpUserType; +import com.red.circle.framework.mybatis.constant.PageConstant; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile; +import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileService; +import com.red.circle.other.inner.enums.team.TeamMemberRole; import com.red.circle.other.inner.model.cmd.team.StatusTeamBatchCmd; import com.red.circle.other.inner.model.cmd.team.TeamTableQryCmd; import com.red.circle.other.inner.model.dto.agency.agency.TeamContact; @@ -61,9 +63,7 @@ public class TeamProfileServiceImpl implements TeamProfileService { @Override public boolean existsAvailableOwnUserId(Long ownUserId) { - return mongoTemplate.exists(Query.query(Criteria.where("ownUserId").is(ownUserId) - .and("status").is(TeamStatus.AVAILABLE)), - TeamProfile.class); + return Objects.nonNull(getAvailableByOwnUserId(ownUserId)); } @Override @@ -236,12 +236,14 @@ public class TeamProfileServiceImpl implements TeamProfileService { criteria.andOperator(andOperator); } - return mongoTemplate.find(Query.query(criteria) - .with(Sort.by(Sort.Order.desc("id"))) - .limit( - Objects.nonNull(query.getLimit()) ? query.getLimit() : PageConstant.DEFAULT_LIMIT_SIZE), - TeamProfile.class); - } + List teamProfiles = mongoTemplate.find(Query.query(criteria) + .with(Sort.by(Sort.Order.desc("id"))) + .limit( + Objects.nonNull(query.getLimit()) ? query.getLimit() : PageConstant.DEFAULT_LIMIT_SIZE), + TeamProfile.class); + + return filterCurrentAvailableOwnProfiles(teamProfiles); + } @Override public List listRegion(String region, Integer limit) { @@ -285,9 +287,59 @@ public class TeamProfileServiceImpl implements TeamProfileService { @Override public TeamProfile getAvailableByOwnUserId(Long ownUserId) { - return mongoTemplate.findOne(Query.query(Criteria.where("ownUserId").is(ownUserId) - .and("status").is(TeamStatus.AVAILABLE)), + List teamProfiles = mongoTemplate.find( + Query.query(Criteria.where("ownUserId").is(ownUserId) + .and("status").is(TeamStatus.AVAILABLE)) + .with(Sort.by(Sort.Order.desc("id"))), TeamProfile.class); + + return filterCurrentAvailableOwnProfiles(teamProfiles).stream().findFirst().orElse(null); + } + + private List filterCurrentAvailableOwnProfiles(List teamProfiles) { + if (CollectionUtils.isEmpty(teamProfiles)) { + return teamProfiles; + } + + List availableProfiles = teamProfiles.stream() + .filter(teamProfile -> TeamStatus.AVAILABLE.equals(teamProfile.getStatus())) + .collect(Collectors.toList()); + + if (CollectionUtils.isEmpty(availableProfiles)) { + return teamProfiles; + } + + Set teamIds = availableProfiles.stream() + .map(TeamProfile::getId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Set ownUserIds = availableProfiles.stream() + .map(TeamProfile::getOwnUserId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (CollectionUtils.isEmpty(teamIds) || CollectionUtils.isEmpty(ownUserIds)) { + return teamProfiles.stream() + .filter(teamProfile -> !TeamStatus.AVAILABLE.equals(teamProfile.getStatus())) + .collect(Collectors.toList()); + } + + Set currentOwnerKeys = mongoTemplate.find(Query.query(Criteria.where("teamId").in(teamIds) + .and("memberId").in(ownUserIds) + .and("role").is(TeamMemberRole.OWN)), + TeamMember.class) + .stream() + .map(teamMember -> ownerKey(teamMember.getTeamId(), teamMember.getMemberId())) + .collect(Collectors.toSet()); + + return teamProfiles.stream() + .filter(teamProfile -> !TeamStatus.AVAILABLE.equals(teamProfile.getStatus()) + || currentOwnerKeys.contains(ownerKey(teamProfile.getId(), teamProfile.getOwnUserId()))) + .collect(Collectors.toList()); + } + + private String ownerKey(Long teamId, Long ownUserId) { + return teamId + "_" + ownUserId; } @Override diff --git a/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamMemberServiceImplTest.java b/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamMemberServiceImplTest.java new file mode 100644 index 00000000..1b2c3c68 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamMemberServiceImplTest.java @@ -0,0 +1,41 @@ +package com.red.circle.other.infra.database.mongo.service.team.team.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile; +import com.red.circle.other.inner.model.dto.agency.agency.TeamStatus; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; + +class TeamMemberServiceImplTest { + + @Test + void getByMemberIdShouldPreferAvailableTeamMemberWhenHistoricalRowsExist() { + MongoTemplate mongoTemplate = mock(MongoTemplate.class); + TeamMemberServiceImpl service = new TeamMemberServiceImpl(mongoTemplate); + + TeamMember closedMember = member(1L, 100L, 10L); + TeamMember availableMember = member(2L, 100L, 20L); + + when(mongoTemplate.find(any(Query.class), eq(TeamMember.class))) + .thenReturn(List.of(closedMember, availableMember)); + when(mongoTemplate.find(any(Query.class), eq(TeamProfile.class))) + .thenReturn(List.of(new TeamProfile().setId(20L).setStatus(TeamStatus.AVAILABLE))); + + assertEquals(availableMember, service.getByMemberId(100L)); + } + + private TeamMember member(Long sortId, Long memberId, Long teamId) { + return new TeamMember() + .setSortId(sortId) + .setMemberId(memberId) + .setTeamId(teamId); + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamProfileServiceImplTest.java b/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamProfileServiceImplTest.java new file mode 100644 index 00000000..9defda94 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/database/mongo/service/team/team/impl/TeamProfileServiceImplTest.java @@ -0,0 +1,66 @@ +package com.red.circle.other.infra.database.mongo.service.team.team.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile; +import com.red.circle.other.inner.enums.team.TeamMemberRole; +import com.red.circle.other.inner.model.cmd.team.TeamTableQryCmd; +import com.red.circle.other.inner.model.dto.agency.agency.TeamStatus; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; + +class TeamProfileServiceImplTest { + + @Test + void listShouldHideAvailableProfileWithoutCurrentOwnerMember() { + MongoTemplate mongoTemplate = mock(MongoTemplate.class); + TeamProfileServiceImpl service = new TeamProfileServiceImpl(mongoTemplate); + + TeamProfile orphanAvailable = profile(1L, 10L, TeamStatus.AVAILABLE); + TeamProfile currentAvailable = profile(2L, 20L, TeamStatus.AVAILABLE); + TeamProfile closed = profile(3L, 30L, TeamStatus.CLOSE); + + when(mongoTemplate.find(any(Query.class), eq(TeamProfile.class))) + .thenReturn(List.of(orphanAvailable, currentAvailable, closed)); + when(mongoTemplate.find(any(Query.class), eq(TeamMember.class))) + .thenReturn(List.of(member(2L, 20L, TeamMemberRole.OWN))); + + List result = service.list(new TeamTableQryCmd()); + + assertEquals(List.of(currentAvailable, closed), result); + } + + @Test + void getAvailableByOwnUserIdShouldIgnoreProfileWithoutCurrentOwnerMember() { + MongoTemplate mongoTemplate = mock(MongoTemplate.class); + TeamProfileServiceImpl service = new TeamProfileServiceImpl(mongoTemplate); + + when(mongoTemplate.find(any(Query.class), eq(TeamProfile.class))) + .thenReturn(List.of(profile(1L, 10L, TeamStatus.AVAILABLE))); + when(mongoTemplate.find(any(Query.class), eq(TeamMember.class))).thenReturn(List.of()); + + assertNull(service.getAvailableByOwnUserId(10L)); + } + + private TeamProfile profile(Long teamId, Long ownUserId, TeamStatus status) { + return new TeamProfile() + .setId(teamId) + .setOwnUserId(ownUserId) + .setStatus(status); + } + + private TeamMember member(Long teamId, Long memberId, TeamMemberRole role) { + return new TeamMember() + .setTeamId(teamId) + .setMemberId(memberId) + .setRole(role); + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/gateway/user/UserRegionGatewayImplTest.java b/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/gateway/user/UserRegionGatewayImplTest.java index b2d20b15..0aaee1bb 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/gateway/user/UserRegionGatewayImplTest.java +++ b/rc-service/rc-service-other/other-infrastructure/src/test/java/com/red/circle/other/infra/gateway/user/UserRegionGatewayImplTest.java @@ -76,9 +76,11 @@ class UserRegionGatewayImplTest { region("SA", "IN,BD", "en,hi"), region("OTHER", "", "") )); - when(userProfileGateway.listByUserIds(Set.of(USER_ID))).thenReturn(List.of( - new UserProfile().setId(USER_ID).setOriginSys(SYS_ORIGIN).setCountryCode("JP") - )); + UserProfile userProfile = new UserProfile(); + userProfile.setId(USER_ID); + userProfile.setOriginSys(SYS_ORIGIN); + userProfile.setCountryCode("JP"); + when(userProfileGateway.listByUserIds(Set.of(USER_ID))).thenReturn(List.of(userProfile)); when(countryCodeAliasSupport.containsCode("IN,BD", "JP")).thenReturn(false); } diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamProfileClientServiceImpl.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamProfileClientServiceImpl.java index f398bb5b..7a74fb0b 100644 --- a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamProfileClientServiceImpl.java +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamProfileClientServiceImpl.java @@ -211,10 +211,10 @@ public class TeamProfileClientServiceImpl implements TeamProfileClientService { ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, userProfile); ResponseAssert.isTrue(UserErrorCode.USER_INFO_NOT_FOUND, Objects.equals(userProfile.getOriginSys(), cmd.getSysOrigin())); - ResponseAssert.isFalse(TeamErrorCode.USER_ALREADY_TEAM_OWN, - teamProfileService.existsOwnUserId(userProfile.getId())); - ResponseAssert.isFalse(TeamErrorCode.USER_ALREADY_TEAM_MEMBER, - teamMemberService.existsMember(userProfile.getId())); + ResponseAssert.isFalse(TeamErrorCode.USER_ALREADY_TEAM_OWN, + teamProfileService.existsAvailableOwnUserId(userProfile.getId())); + ResponseAssert.isFalse(TeamErrorCode.USER_ALREADY_TEAM_MEMBER, + existsAvailableTeamMember(userProfile.getId())); // 验证BD关系 if (Objects.nonNull(cmd.getBdUserId())) { @@ -338,10 +338,22 @@ public class TeamProfileClientServiceImpl implements TeamProfileClientService { userMqMessageService.sendBadgeOperateEvent(userProfile.getId(), BadgeKeyEnum.AGENCY.name(), BadgeOperateEvent.OperationType.WEAR.name()); userMqMessageService.sendBadgeOperateEvent(userProfile.getId(), BadgeKeyEnum.HOST.name(), BadgeOperateEvent.OperationType.WEAR.name()); - redisService.unlock(key); - } - - private TeamProfileDTO TeamProfileDTOConvertor(TeamCreateCmd cmd) { + redisService.unlock(key); + } + + private boolean existsAvailableTeamMember(Long userId) { + TeamMember teamMember = teamMemberService.getByMemberId(userId); + if (Objects.isNull(teamMember)) { + return false; + } + + TeamProfile teamProfile = teamProfileService.getById(teamMember.getTeamId()); + return Objects.nonNull(teamProfile) + && TeamStatus.AVAILABLE.equals(teamProfile.getStatus()) + && teamProfileService.existsAvailableOwnUserId(teamProfile.getOwnUserId()); + } + + private TeamProfileDTO TeamProfileDTOConvertor(TeamCreateCmd cmd) { return new TeamProfileDTO() .setRegion(cmd.getRegion()) .setSysOrigin(cmd.getSysOrigin())