diff --git a/rc-auth/pom.xml b/rc-auth/pom.xml index cc696de3..46485c32 100644 --- a/rc-auth/pom.xml +++ b/rc-auth/pom.xml @@ -58,6 +58,16 @@ ${project.name}-${revision} + + org.apache.maven.plugins + maven-resources-plugin + + + + mmdb + + + spring-boot-maven-plugin org.springframework.boot diff --git a/rc-auth/src/main/java/com/red/circle/auth/endpoint/EndpointRestController.java b/rc-auth/src/main/java/com/red/circle/auth/endpoint/EndpointRestController.java index 0c0a869d..b459b893 100644 --- a/rc-auth/src/main/java/com/red/circle/auth/endpoint/EndpointRestController.java +++ b/rc-auth/src/main/java/com/red/circle/auth/endpoint/EndpointRestController.java @@ -310,6 +310,11 @@ public class EndpointRestController { } } log.info("IP:{}, 国家信息:{}", ip, dataObject); + // IP 归属接口超时或返回异常时不能让登录链路 NPE,中港台拦截只在拿到明确国家信息时执行。 + if (dataObject == null) { + log.warn("IP:{} 国家信息获取失败,跳过 IP 国家校验", ip); + return null; + } if (dataObject != null && "中国".equals(dataObject.getString("country"))) { if (!Arrays.asList("香港", "澳门", "台湾").contains(dataObject.getString("region"))) { ResponseAssert.isTrue(UserErrorCode.REGISTRATION_FAILED, false); diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBaseInfoServiceImpl.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBaseInfoServiceImpl.java index e26adad7..2237f7ca 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBaseInfoServiceImpl.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBaseInfoServiceImpl.java @@ -36,6 +36,7 @@ import com.red.circle.other.inner.endpoint.user.user.UserProfileClient; import com.red.circle.other.inner.enums.material.NobleVipEnum; import com.red.circle.other.inner.enums.material.PropsCommodityType; import com.red.circle.other.inner.enums.team.TeamMemberRole; +import com.red.circle.other.inner.asserts.user.UserErrorCode; import com.red.circle.other.inner.model.cmd.count.PropsSaleQuotaQryCmd; import com.red.circle.other.inner.model.cmd.count.SysOriginUserQryCmd; import com.red.circle.other.inner.model.cmd.tools.UserRoomRegionUpdateCmd; @@ -199,6 +200,15 @@ public class UserBaseInfoServiceImpl implements if (Objects.isNull(param)) { return; } + UserProfileDTO currentProfile = ResponseAssert.requiredSuccess( + userProfileClient.getByUserId(param.getId())); + ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, currentProfile); + + boolean countryChanged = countryChanged(param, currentProfile); + if (countryChanged) { + fillCountryAndAssertAllowed(param, currentProfile); + } + userProfileClient.updateSelectiveById(userAppConvertor.toUserBaseInfoCmd(param)); // 移除所有缓存 userProfileClient.removeCacheAll(param.getId()); @@ -207,7 +217,61 @@ public class UserBaseInfoServiceImpl implements setUserIds(List.of(param.getId())) .setEvent(NewsletterEvent.USER_INFO_CHANGE) ); - roomManagerClient.updateUserCountry(param.getId(), param.getCountryCode(), param.getCountryName()); + if (countryChanged) { + roomManagerClient.updateUserCountry(param.getId(), param.getCountryCode(), + param.getCountryName()); + } + } + + private boolean countryChanged(UserProfileUpdateCmd param, UserProfileDTO currentProfile) { + return Objects.nonNull(param.getCountryId()) + && !Objects.equals(param.getCountryId(), currentProfile.getCountryId()); + } + + private void fillCountryAndAssertAllowed(UserProfileUpdateCmd param, + UserProfileDTO currentProfile) { + SysCountryCodeDTO country = ResponseAssert.requiredSuccess( + countryCodeClient.getById(param.getCountryId())); + ResponseAssert.notNull(UserErrorCode.SYS_REGION_NOT_EXIST_ERROR, country); + + boolean locked = hasCountryLockedIdentity(param.getId()); + log.warn("console country-change identity check userId={}, locked={}, currentCountryId={}, targetCountryId={}", + param.getId(), locked, currentProfile.getCountryId(), param.getCountryId()); + // 国家变更只限制平台身份用户;普通用户允许后台多次调整并同步房间国家。 + ResponseAssert.isFalse(UserErrorCode.USER_COUNTRY_UPDATE_NOT_ALLOWED, locked); + + param.setCountryCode(country.getAlphaTwo()); + param.setCountryName(country.getCountryName()); + } + + private boolean hasCountryLockedIdentity(Long userId) { + TeamMemberDTO teamMember = getTeamMember(userId); + boolean anchor = Objects.nonNull(teamMember); + boolean agent = anchor && TeamMemberRole.OWN.eq(teamMember.getRole()); + boolean bd = checkBd(userId); + boolean bdLeader = checkBdLeader(userId); + log.warn("console country-change identity detail userId={}, anchor={}, agent={}, bd={}, bdLeader={}, teamMemberRole={}", + userId, anchor, agent, bd, bdLeader, + Objects.isNull(teamMember) ? null : teamMember.getRole()); + return anchor || agent || bd || bdLeader; + } + + private TeamMemberDTO getTeamMember(Long userId) { + try { + return ResponseAssert.requiredSuccess(teamProfileClient.getByMemberId(userId)); + } catch (Exception ex) { + log.warn("console country-change team member check failed, treat as no team member, userId={}", + userId, ex); + return null; + } + } + + private boolean checkBd(Long userId) { + return Boolean.TRUE.equals(ResponseAssert.requiredSuccess(bdTeamInfoClient.check(userId))); + } + + private boolean checkBdLeader(Long userId) { + return Boolean.TRUE.equals(ResponseAssert.requiredSuccess(bdTeamLeaderClient.check(userId))); } @Override diff --git a/rc-service/rc-service-external/external-infrastructure/src/main/java/com/red/circle/external/infra/oss/TencentCosOssService.java b/rc-service/rc-service-external/external-infrastructure/src/main/java/com/red/circle/external/infra/oss/TencentCosOssService.java index ae553477..c59e57e3 100644 --- a/rc-service/rc-service-external/external-infrastructure/src/main/java/com/red/circle/external/infra/oss/TencentCosOssService.java +++ b/rc-service/rc-service-external/external-infrastructure/src/main/java/com/red/circle/external/infra/oss/TencentCosOssService.java @@ -131,6 +131,10 @@ public class TencentCosOssService implements OssService { if (StringUtils.isAnyBlank(sourceKey, targetKey)) { return; } + if (StringUtils.equals(sourceKey, targetKey)) { + log.warn("Skip COS image process because source and target key are the same, key={}", sourceKey); + return; + } try { PicOperations picOperations = new PicOperations(); @@ -165,7 +169,18 @@ public class TencentCosOssService implements OssService { @Override public String processImgSaveAsCompressZoom(String source, int height) { - return processImgSaveAsCompressZoom(source, source, height); + if (isHttpUrl(source)) { + return source; + } + if (StringUtils.endsWith(source, ".gift")) { + return getAccessUrl(source); + } + String sourceKey = normalizeKey(source); + if (StringUtils.isBlank(sourceKey)) { + return ""; + } + // COS does not allow persistent image processing to save over the source object. + return processImgSaveAsCompressZoom(sourceKey, buildProcessedImageKey(sourceKey, height), height); } private void uploadInputStream(InputStream inputStream, String fileName, ObjectMetadata metadata) { @@ -203,6 +218,17 @@ public class TencentCosOssService implements OssService { return "imageMogr2/thumbnail/x1024"; } + private String buildProcessedImageKey(String sourceKey, int height) { + String key = normalizeKey(sourceKey); + String suffix = "_x" + height; + String extension = StringUtils.substringAfterLast(key, "."); + String baseName = StringUtils.substringBeforeLast(key, "."); + if (StringUtils.isBlank(extension) || StringUtils.equals(baseName, key)) { + return key + suffix; + } + return baseName + suffix + "." + extension; + } + private String getFilePath(String urlOrKey) { if (StringUtils.isBlank(urlOrKey)) { return urlOrKey; diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/gift/GiftGiveAwayBatchCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/gift/GiftGiveAwayBatchCmdExe.java index d8b31c05..f360b6ce 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/gift/GiftGiveAwayBatchCmdExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/gift/GiftGiveAwayBatchCmdExe.java @@ -79,7 +79,8 @@ public class GiftGiveAwayBatchCmdExe { checkDynamicRegion(cmd); if (giftConfig.getGiftCandy().equals(new BigDecimal("0.00"))) { - BigDecimal dollarAmount = walletGoldClient.getBalance(cmd.requiredReqUserId()).getBody().getDollarAmount(); + BigDecimal dollarAmount = ResponseAssert.requiredSuccess( + walletGoldClient.getBalance(cmd.requiredReqUserId())).getDollarAmount(); log.info("赠送普通礼物金额为0,余额为 {}", dollarAmount); return dollarAmount; } @@ -115,7 +116,8 @@ public class GiftGiveAwayBatchCmdExe { checkPrivateAcceptUser(cmd); if (isZeroGiftCandy(giftConfig)) { - BigDecimal dollarAmount = walletGoldClient.getBalance(cmd.requiredReqUserId()).getBody().getDollarAmount(); + BigDecimal dollarAmount = ResponseAssert.requiredSuccess( + walletGoldClient.getBalance(cmd.requiredReqUserId())).getDollarAmount(); log.info("私聊赠送普通礼物金额为0,余额为 {}", dollarAmount); return dollarAmount; } @@ -175,7 +177,9 @@ public class GiftGiveAwayBatchCmdExe { if (isGiftTypeEqGold(giftConfig)) { BigDecimal amount = cmd.calculateConsumeGolds(giftConfig.getGiftCandy()); // 消耗金币 - WalletReceiptResDTO receiptRes = walletGoldClient.changeBalance(GoldReceiptCmd.builder() + // 钱包返回余额不足等业务错误时必须直接透传,避免空 body 继续组装礼物流水触发 500。 + WalletReceiptResDTO receiptRes = ResponseAssert.requiredSuccess( + walletGoldClient.changeBalance(GoldReceiptCmd.builder() .appExpenditure() .userId(cmd.requiredReqUserId()) .eventId(giftConfig.getId()) @@ -183,7 +187,7 @@ public class GiftGiveAwayBatchCmdExe { .origin(GoldOrigin.GIVE_GIFT + "_" + giftConfig.getGiftTab()) .amount(amount) .closeDelayAsset() - .build()).getBody(); + .build())); return new WalletReceiptDTO() .setConsumeId(receiptRes.getAssetRecordId()) .setBalance(receiptRes.getBalance().getDollarAmount()) @@ -192,7 +196,7 @@ public class GiftGiveAwayBatchCmdExe { if (isGiftTypeEqDiamond(giftConfig)) { // 消费积分 - return walletDiamondClient.changeBalance(new DiamondReceiptCmd() + return ResponseAssert.requiredSuccess(walletDiamondClient.changeBalance(new DiamondReceiptCmd() .setConsumeId(Objects.toString(giftConfig.getId())) .setType(ReceiptType.EXPENDITURE) .setTrackId(giftConfig.getId()) @@ -201,7 +205,7 @@ public class GiftGiveAwayBatchCmdExe { .setOrigin(DiamondOrigin.GIVE_GIFT) .setAmount(cmd.calculateConsumeGolds(giftConfig.getGiftCandy())) .setCreateTime(TimestampUtils.now()) - ).getBody(); + )); } // 类型不在范围 @@ -226,7 +230,9 @@ public class GiftGiveAwayBatchCmdExe { if (isGiftTypeEqGold(giftConfig)) { BigDecimal amount = cmd.calculateConsumeGolds(giftConfig.getGiftCandy()); - WalletReceiptResDTO receiptRes = walletGoldClient.changeBalance(GoldReceiptCmd.builder() + // 钱包返回余额不足等业务错误时必须直接透传,避免空 body 继续组装私信礼物流水触发 500。 + WalletReceiptResDTO receiptRes = ResponseAssert.requiredSuccess( + walletGoldClient.changeBalance(GoldReceiptCmd.builder() .appExpenditure() .userId(cmd.requiredReqUserId()) .eventId(giftConfig.getId()) @@ -234,7 +240,7 @@ public class GiftGiveAwayBatchCmdExe { .origin(GoldOrigin.GIVE_GIFT + "_" + giftConfig.getGiftTab()) .amount(amount) .closeDelayAsset() - .build()).getBody(); + .build())); return new WalletReceiptDTO() .setConsumeId(receiptRes.getAssetRecordId()) .setBalance(receiptRes.getBalance().getDollarAmount()) @@ -242,7 +248,7 @@ public class GiftGiveAwayBatchCmdExe { } if (isGiftTypeEqDiamond(giftConfig)) { - return walletDiamondClient.changeBalance(new DiamondReceiptCmd() + return ResponseAssert.requiredSuccess(walletDiamondClient.changeBalance(new DiamondReceiptCmd() .setConsumeId(Objects.toString(giftConfig.getId())) .setType(ReceiptType.EXPENDITURE) .setTrackId(giftConfig.getId()) @@ -251,7 +257,7 @@ public class GiftGiveAwayBatchCmdExe { .setOrigin(DiamondOrigin.GIVE_GIFT) .setAmount(cmd.calculateConsumeGolds(giftConfig.getGiftCandy())) .setCreateTime(TimestampUtils.now()) - ).getBody(); + )); } ResponseAssert.failure(CommonErrorCode.TYPE_IS_NOT_IN_SCOPE); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/room/query/RoomVoiceExploreQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/room/query/RoomVoiceExploreQryExe.java index 4b1e665c..50867fa3 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/room/query/RoomVoiceExploreQryExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/room/query/RoomVoiceExploreQryExe.java @@ -1,5 +1,6 @@ package com.red.circle.other.app.command.room.query; +import com.alibaba.fastjson.JSON; import com.red.circle.other.app.common.room.RoomRegionCountryCommon; import com.red.circle.other.app.common.room.RoomRegionCountryCommon.RegionCountryScope; import com.red.circle.other.app.common.room.RoomVoiceProfileCommon; @@ -10,11 +11,19 @@ import com.red.circle.other.app.service.room.RocketStatusCacheService; import com.red.circle.other.domain.rocket.RocketStatus; import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom; import com.red.circle.other.infra.database.mongo.service.live.ActiveVoiceRoomService; +import com.red.circle.other.inner.model.dto.live.ActiveVoiceRoomCO; import com.red.circle.tool.core.collection.CollectionUtils; +import com.red.circle.tool.core.text.StringUtils; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; /** @@ -24,15 +33,18 @@ import org.springframework.stereotype.Component; */ @Component @RequiredArgsConstructor +@Slf4j public class RoomVoiceExploreQryExe { private static final int EXPLORE_LIMIT = 100; + private static final String EMPTY_ROOM_CACHE_KEY = "empty_room_cache:*"; private final ActiveVoiceRoomService activeVoiceRoomService; private final RoomVoiceProfileCommon roomVoiceProfileCommon; private final RocketStatusCacheService rocketStatusCacheService; private final GameLudoService gameLudoService; private final RoomRegionCountryCommon roomRegionCountryCommon; + private final RedisTemplate redisTemplate; public List execute(RoomExploreQryCmd cmd) { RegionCountryScope regionScope = roomRegionCountryCommon.resolve( @@ -42,6 +54,8 @@ public class RoomVoiceExploreQryExe { List rooms = activeVoiceRoomService.listExplore( cmd.requireReqSysOrigin(), regionScope.getRegionCode(), countryCode, regionScope.getCountryCodes(), EXPLORE_LIMIT); + rooms = appendEmptyRooms( + rooms, cmd.requireReqSysOrigin(), regionScope.getCountryCodes(), countryCode); List roomList = roomVoiceProfileCommon.toListRoomVoiceProfileCO(rooms); if (CollectionUtils.isEmpty(roomList)) { @@ -54,6 +68,92 @@ public class RoomVoiceExploreQryExe { return roomList; } + private List appendEmptyRooms( + List rooms, String sysOrigin, Set visibleCountryCodes, + String selectedCountryCode) { + List emptyRooms = getEmptyRoomsFromCache( + sysOrigin, visibleCountryCodes, selectedCountryCode); + if (CollectionUtils.isEmpty(emptyRooms)) { + return rooms; + } + + List mergedRooms = CollectionUtils.isEmpty(rooms) + ? new ArrayList<>() + : new ArrayList<>(rooms); + Set existingRoomIds = CollectionUtils.isEmpty(rooms) + ? Set.of() + : rooms.stream().map(ActiveVoiceRoom::getId).collect(Collectors.toSet()); + emptyRooms.stream() + .filter(room -> !existingRoomIds.contains(room.getId())) + .forEach(mergedRooms::add); + return mergedRooms; + } + + private List getEmptyRoomsFromCache( + String sysOrigin, Set visibleCountryCodes, String selectedCountryCode) { + try { + Set keys = redisTemplate.keys(EMPTY_ROOM_CACHE_KEY); + if (CollectionUtils.isEmpty(keys)) { + return CollectionUtils.newArrayList(); + } + + List emptyRooms = new ArrayList<>(); + for (String key : keys) { + Object cachedRoom = redisTemplate.opsForValue().get(key); + ActiveVoiceRoom room = convertToActiveVoiceRoom(cachedRoom); + if (isVisibleEmptyRoom(room, sysOrigin, visibleCountryCodes, selectedCountryCode)) { + emptyRooms.add(room); + } + } + return emptyRooms; + } catch (Exception e) { + log.warn("获取国家筛选空房间缓存失败", e); + return CollectionUtils.newArrayList(); + } + } + + private boolean isVisibleEmptyRoom( + ActiveVoiceRoom room, String sysOrigin, Set visibleCountryCodes, + String selectedCountryCode) { + if (room == null || !Objects.equals(sysOrigin, room.getSysOrigin())) { + return false; + } + + String roomCountryCode = roomRegionCountryCommon.normalizeCountryCode(room.getCountryCode()); + if (!visibleCountryCodes.contains(roomCountryCode)) { + return false; + } + + // 国家筛选页需要和首页 hot 使用同一套空房缓存,否则退出房间后的 10 分钟内两边数量会不一致。 + return StringUtils.isBlank(selectedCountryCode) + || Objects.equals(selectedCountryCode, roomCountryCode); + } + + private ActiveVoiceRoom convertToActiveVoiceRoom(Object cachedRoom) { + if (cachedRoom == null) { + return null; + } + try { + ActiveVoiceRoomCO roomCO = JSON.parseObject(String.valueOf(cachedRoom), ActiveVoiceRoomCO.class); + if (roomCO == null) { + return null; + } + ActiveVoiceRoom room = new ActiveVoiceRoom(); + BeanUtils.copyProperties(roomCO, room); + room.setOnlineQuantity(0L); + if (room.getWeights() == null) { + room.setWeights(0); + } + if (room.getFixedWeights() == null) { + room.setFixedWeights(0); + } + return room; + } catch (Exception e) { + log.warn("转换国家筛选空房间缓存失败", e); + return null; + } + } + private void fillRocketStatus(List roomList) { List roomIds = roomList.stream().map(RoomVoiceProfileCO::getId).toList(); Map rocketStatusMap = rocketStatusCacheService.batchGetRocketStatus(roomIds); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/AccountBindUserAuthTypeCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/AccountBindUserAuthTypeCmdExe.java index 5cefe842..21d8c176 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/AccountBindUserAuthTypeCmdExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/AccountBindUserAuthTypeCmdExe.java @@ -5,6 +5,7 @@ import com.red.circle.component.redis.service.RedisService; import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.core.response.CommonErrorCode; import com.red.circle.other.app.dto.cmd.user.user.AccountBindUserAuthTypeCmd; +import com.red.circle.other.infra.database.cache.service.user.UserAccountAuthCacheService; import com.red.circle.other.infra.database.rds.entity.user.user.AccountAuth; import com.red.circle.other.infra.database.rds.entity.user.user.AuthType; import com.red.circle.other.infra.database.rds.entity.user.user.PasswordLog; @@ -34,6 +35,7 @@ public class AccountBindUserAuthTypeCmdExe { private final AuthTypeService authTypeService; private final AccountAuthService accountAuthService; private final PasswordLogService passwordLogService; + private final UserAccountAuthCacheService accountAuthCacheService; public void execute(AccountBindUserAuthTypeCmd cmd) { AuthType authType = authTypeService.getAuthInfo( @@ -58,6 +60,8 @@ public class AccountBindUserAuthTypeCmdExe { ); authTypeService.save(toAuthType(cmd.requiredReqUserId(), cmd)); + // 账号密码设置成功后清理旧的登录失败次数,避免新密码立即被历史错误次数拦截。 + accountAuthCacheService.deleteByUserId(cmd.requiredReqUserId()); } public void accountUpdate(AccountBindUserAuthTypeCmd cmd) { @@ -85,6 +89,9 @@ public class AccountBindUserAuthTypeCmdExe { .pwd(cmd.getPwd()) .imei(cmd.getReqImei()) .build()); + + // 修改密码成功后清理旧的登录失败次数,确保用户可以立即用新密码登录。 + accountAuthCacheService.deleteByUserId(cmd.requiredReqUserId()); } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/UpdateUserProfileCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/UpdateUserProfileCmdExe.java index 9caacc7a..71fc4156 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/UpdateUserProfileCmdExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/UpdateUserProfileCmdExe.java @@ -10,11 +10,17 @@ import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.core.response.CommonErrorCode; import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; import com.red.circle.other.app.dto.cmd.user.user.UserProfileModifyCmd; +import com.red.circle.other.app.service.user.user.UserIdentityService; import com.red.circle.other.domain.gateway.approval.ProfileApprovalGateway; import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway; import com.red.circle.other.domain.model.approval.ProfileApprovalContent; import com.red.circle.other.domain.model.user.UserProfile; import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService; +import com.red.circle.other.infra.database.mongo.service.live.ActiveVoiceRoomService; +import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService; +import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode; +import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService; import com.red.circle.other.inner.asserts.DynamicErrorCode; import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.text.StringUtils; @@ -25,7 +31,9 @@ import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; @@ -47,6 +55,11 @@ public class UpdateUserProfileCmdExe { private final ProfileApprovalGateway profileApprovalGateway; private final UserProfileAppConvertor userProfileAppConvertor; private final CpRelationshipCacheService cpRelationshipCacheService; + private final UserIdentityService userIdentityService; + private final SysCountryCodeService sysCountryCodeService; + private final RoomProfileManagerService roomProfileManagerService; + private final ActiveVoiceRoomService activeVoiceRoomService; + private final UserRegionGateway userRegionGateway; public UserProfileDTO execute(UserProfileModifyCmd cmd) { //敏感词 @@ -56,7 +69,7 @@ public class UpdateUserProfileCmdExe { userProfileGateway.removeCacheAll(cmd.requiredReqUserId()); UserProfile userProfile = userProfileGateway.getByUserId(cmd.requiredReqUserId()); ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, userProfile); - assertCountryNotChanged(cmd, userProfile); + SysCountryCode changeCountry = getChangeCountry(cmd, userProfile); if (StringUtils.isNotBlank(cmd.getUserAvatar())) { cmd.setUserAvatar(ossServiceClient.processImgSaveAsCompressZoom(cmd.getUserAvatar(), ImageSizeConst.COVER_HEIGHT).getBody() @@ -66,7 +79,13 @@ public class UpdateUserProfileCmdExe { UserProfile updateUserProfile = userProfileAppConvertor.toUserProfile(cmd); ResponseAssert.notNull(CommonErrorCode.UPDATE_FAILURE, updateUserProfile); updateUserProfile.setId(userProfile.getId()); - updateUserProfile.setCountryId(null); + if (Objects.nonNull(changeCountry)) { + updateUserProfile.setCountryId(changeCountry.getId()); + updateUserProfile.setCountryCode(changeCountry.getAlphaTwo()); + updateUserProfile.setCountryName(changeCountry.getCountryName()); + } else { + updateUserProfile.setCountryId(null); + } // 处理背景照片:如果传入非null,则更新(包括空列表) if (cmd.getBackgroundPhotos() != null) { @@ -101,8 +120,11 @@ public class UpdateUserProfileCmdExe { processPhotoStatus(updateUserProfile.getPersonalPhotos(), userProfile.getPersonalPhotos()); userProfileGateway.updateSelectiveById(updateUserProfile); + if (Objects.nonNull(changeCountry)) { + syncRoomCountry(userProfile.getId(), changeCountry); + } - syncProperty(userProfile, cmd); + syncProperty(userProfile, cmd, changeCountry); log.warn("cmd: {},{}", cmd, userProfile); updateUserProfile.setOriginSys(cmd.requireReqSysOrigin()); submitApproval(updateUserProfile); @@ -112,12 +134,31 @@ public class UpdateUserProfileCmdExe { return userProfileAppConvertor.toUserProfileDTO(userProfile); } - private void assertCountryNotChanged(UserProfileModifyCmd cmd, UserProfile userProfile) { + private SysCountryCode getChangeCountry(UserProfileModifyCmd cmd, UserProfile userProfile) { // App 保存资料可能会携带当前国家 id,只拦截真实变更,避免昵称、头像等普通资料保存被误伤。 - if (Objects.nonNull(cmd.getCountryId()) - && !Objects.equals(userProfile.getCountryId(), cmd.getCountryId())) { - ResponseAssert.failure(UserErrorCode.USER_COUNTRY_UPDATE_NOT_ALLOWED); + if (Objects.isNull(cmd.getCountryId()) + || Objects.equals(userProfile.getCountryId(), cmd.getCountryId())) { + return null; } + + Map countryMap = sysCountryCodeService.mapByIdes(Set.of(cmd.getCountryId())); + SysCountryCode country = countryMap.get(cmd.getCountryId()); + ResponseAssert.notNull(UserErrorCode.SYS_REGION_NOT_EXIST_ERROR, country); + assertAllowCountryChange(userProfile.getId()); + return country; + } + + private void assertAllowCountryChange(Long userId) { + ResponseAssert.isFalse(UserErrorCode.USER_COUNTRY_UPDATE_NOT_ALLOWED, + userIdentityService.hasCountryLockedIdentity(userId)); + } + + private void syncRoomCountry(Long userId, SysCountryCode country) { + userProfileGateway.removeCacheAll(userId); + String regionCode = userRegionGateway.getRegionCode(userId); + roomProfileManagerService.updateUserCountry(userId, country.getAlphaTwo(), country.getCountryName()); + activeVoiceRoomService.updateUserCountry(userId, country.getAlphaTwo(), country.getCountryName(), + regionCode); } private void submitApproval(UserProfile updateUserProfile) { @@ -183,7 +224,7 @@ public class UpdateUserProfileCmdExe { } - private void syncProperty(UserProfile oldProfile, UserProfileModifyCmd cmd) { + private void syncProperty(UserProfile oldProfile, UserProfileModifyCmd cmd, SysCountryCode changeCountry) { if (StringUtils.isNotBlank(cmd.getUserNickname())) { oldProfile.setUserNickname(cmd.getUserNickname()); @@ -204,6 +245,12 @@ public class UpdateUserProfileCmdExe { oldProfile.setUserSex(cmd.getUserSex()); } + if (Objects.nonNull(changeCountry)) { + oldProfile.setCountryId(changeCountry.getId()); + oldProfile.setCountryCode(changeCountry.getAlphaTwo()); + oldProfile.setCountryName(changeCountry.getCountryName()); + } + } private void processAge(UserProfile userProfile) { diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityServiceImpl.java index 5bc86e93..315475a4 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityServiceImpl.java @@ -2,22 +2,21 @@ package com.red.circle.other.app.service.user.user; import com.red.circle.framework.core.asserts.ResponseAssert; -import com.red.circle.framework.mybatis.constant.PageConstant; import com.red.circle.other.app.dto.cmd.material.PropsStoreTypeQryCmd; import com.red.circle.other.app.dto.h5.UserIdentityVO; -import com.red.circle.other.app.service.team.TeamBdService; +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.sys.Administrator; import com.red.circle.other.infra.database.rds.service.sys.AdministratorAuthService; import com.red.circle.other.infra.database.rds.service.sys.AdministratorService; import com.red.circle.other.inner.endpoint.team.bd.BdTeamInfoClient; import com.red.circle.other.inner.endpoint.team.bd.BdTeamLeaderClient; -import com.red.circle.other.inner.endpoint.team.target.TeamProfileClient; import com.red.circle.other.inner.enums.material.PropsCommodityType; import com.red.circle.other.inner.enums.sys.appmanager.RoomRolesEnum; import com.red.circle.other.inner.enums.team.TeamMemberRole; -import com.red.circle.other.inner.model.dto.agency.agency.TeamMemberDTO; import com.red.circle.wallet.inner.endpoint.freight.FreightGoldClient; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; @@ -26,9 +25,10 @@ import java.util.Optional; @Service @RequiredArgsConstructor +@Slf4j public class UserIdentityServiceImpl implements UserIdentityService { - private final TeamProfileClient teamProfileClient; + private final TeamMemberService teamMemberService; private final FreightGoldClient freightGoldClient; private final BdTeamInfoClient bdTeamInfoClient; private final AdministratorService administratorService; @@ -37,8 +37,7 @@ public class UserIdentityServiceImpl implements UserIdentityService { @Override public UserIdentityVO getUserIdentity(Long userId) { - TeamMemberDTO teamMember = ResponseAssert.requiredSuccess( - teamProfileClient.getByMemberId(userId)); + TeamMember teamMember = teamMemberService.getByMemberId(userId); Administrator administrator = administratorService.getByUserId(userId); boolean isValidAdmin = administrator != null && Boolean.TRUE.equals(administrator.getStatus()); @@ -63,6 +62,48 @@ public class UserIdentityServiceImpl implements UserIdentityService { ; } + @Override + public boolean hasCountryLockedIdentity(Long userId) { + TeamMember teamMember = teamMemberService.getByMemberId(userId); + boolean anchor = hasAnchorIdentity(teamMember); + boolean agent = hasAgentIdentity(teamMember); + boolean bd = checkBd(userId); + boolean bdLeader = checkBdLeader(userId); + boolean locked = anchor || agent || bd || bdLeader; + log.warn("country-change identity check userId={}, locked={}, anchor={}, agent={}, bd={}, bdLeader={}, teamMemberId={}, teamMemberRole={}, teamId={}", + userId, locked, anchor, agent, bd, bdLeader, getTeamMemberId(teamMember), + getTeamMemberRole(teamMember), getTeamId(teamMember)); + return locked; + } + + private boolean hasAnchorIdentity(TeamMember teamMember) { + return Objects.nonNull(teamMember); + } + + private boolean hasAgentIdentity(TeamMember teamMember) { + return Objects.nonNull(teamMember) && TeamMemberRole.OWN.eq(teamMember.getRole()); + } + + private boolean checkBd(Long userId) { + return Boolean.TRUE.equals(ResponseAssert.requiredSuccess(bdTeamInfoClient.check(userId))); + } + + private boolean checkBdLeader(Long userId) { + return Boolean.TRUE.equals(ResponseAssert.requiredSuccess(bdTeamLeaderClient.check(userId))); + } + + private String getTeamMemberId(TeamMember teamMember) { + return Objects.isNull(teamMember) ? null : teamMember.getId(); + } + + private TeamMemberRole getTeamMemberRole(TeamMember teamMember) { + return Objects.isNull(teamMember) ? null : teamMember.getRole(); + } + + private Long getTeamId(TeamMember teamMember) { + return Objects.isNull(teamMember) ? null : teamMember.getTeamId(); + } + @Override public Boolean hasPermission(PropsStoreTypeQryCmd cmd) { List authResources = administratorAuthService.listAuthResources(cmd.requiredReqUserId()); diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityService.java index 15e8ebd9..2b0354fb 100644 --- a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityService.java +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityService.java @@ -10,6 +10,11 @@ public interface UserIdentityService { */ UserIdentityVO getUserIdentity(Long userId); + /** + * 是否存在不允许自由修改国家的平台身份. + */ + boolean hasCountryLockedIdentity(Long userId); + /** * 用户是否有指定道具类型的权限 * @param cmd diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/ActiveVoiceRoomService.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/ActiveVoiceRoomService.java index 63a7818c..18ace497 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/ActiveVoiceRoomService.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/ActiveVoiceRoomService.java @@ -44,6 +44,11 @@ public interface ActiveVoiceRoomService { */ void updateQuantityByRoomAccount(String roomAccount, Long quantity); + /** + * 修改用户在线房间国家. + */ + void updateUserCountry(Long userId, String countryCode, String countryName, String region); + /** * 移除首页房间. * diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/impl/ActiveVoiceRoomServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/impl/ActiveVoiceRoomServiceImpl.java index 9adfe55b..d115b6e1 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/impl/ActiveVoiceRoomServiceImpl.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/impl/ActiveVoiceRoomServiceImpl.java @@ -8,6 +8,7 @@ import com.red.circle.other.infra.database.mongo.dto.query.live.FamilyOnlineRoom import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom; import com.red.circle.other.infra.database.mongo.service.live.ActiveVoiceRoomService; import com.red.circle.other.inner.model.cmd.live.AppActiveVoiceRoomQryCmd; +import com.red.circle.tool.core.date.TimestampUtils; import org.springframework.data.mongodb.core.aggregation.Aggregation; import com.red.circle.tool.core.text.StringUtils; @@ -64,6 +65,16 @@ public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService { new Update().set("onlineQuantity", quantity), ActiveVoiceRoom.class); } + @Override + public void updateUserCountry(Long userId, String countryCode, String countryName, String region) { + mongoTemplate.updateMulti(Query.query(Criteria.where("userId").is(userId)), + new Update().set("updateTime", TimestampUtils.now()) + .set("countryCode", StringUtils.toUpperCase(countryCode)) + .set("countryName", countryName) + .set("region", region), + ActiveVoiceRoom.class); + } + @Override public void removeByIds(List ids) { mongoTemplate.remove(Query.query(Criteria.where("id").in(ids)), diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/impl/RoomProfileManagerServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/impl/RoomProfileManagerServiceImpl.java index 650c9d4a..e7679d6c 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/impl/RoomProfileManagerServiceImpl.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/mongo/service/live/impl/RoomProfileManagerServiceImpl.java @@ -514,9 +514,9 @@ public class RoomProfileManagerServiceImpl implements RoomProfileManagerService @Override public void updateUserCountry(Long userId, String countryCode, String countryName) { - mongoTemplate.updateFirst(Query.query(Criteria.where("userId").is(userId)), + mongoTemplate.updateMulti(Query.query(Criteria.where("userId").is(userId)), new Update().set("updateTime", TimestampUtils.now()) - .set("countryCode", countryCode) + .set("countryCode", StringUtils.toUpperCase(countryCode)) .set("countryName", countryName), RoomProfileManager.class); }