feat(other): 国家资料级联同步用户与团队冗余快照

- 新增国家级/用户级 Redis 写锁(token+Lua 安全释放),统一 country -> user 锁序
- 后台改国家码/名称时按 countryId 游标分批同步 RDS 与 mongo 全部国家冗余字段,
  含注册国家码同事务重命名、团队区域重算(CAS + 投影回滚)与两次缓存失效
- 建团(BD 邀请/后台)、改用户资料、注册、编辑团队等写国家快照路径接入锁防竞态,
  团队国家码一律取国家主表标准值
- 违规调整/账号状态改为仅写变更字段,防止旧快照覆盖同步结果
- 团队账单结算改用账单自身 region,配合团队迁区
- 补充 user_base_info (country_id,id) 索引与 mongo 复合索引迁移脚本

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hy001 2026-07-16 20:11:57 +08:00
parent e956250483
commit c884827072
50 changed files with 1969 additions and 359 deletions

View File

@ -14,19 +14,23 @@ import com.red.circle.mq.rocket.business.producer.UserMqMessageService;
import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; import com.red.circle.other.app.convertor.user.UserProfileAppConvertor;
import com.red.circle.other.app.dto.cmd.team.DbInviteMessageProcessCmd; import com.red.circle.other.app.dto.cmd.team.DbInviteMessageProcessCmd;
import com.red.circle.other.app.util.OfficialNoticeUtils; import com.red.circle.other.app.util.OfficialNoticeUtils;
import com.red.circle.other.domain.gateway.user.UserProfileGateway; 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.gateway.user.ability.RegisterDeviceGateway;
import com.red.circle.other.infra.database.mongo.entity.team.bd.BdInviteAgentMessage; import com.red.circle.other.infra.common.sys.CountryDataSyncLockService;
import com.red.circle.other.infra.common.user.UserCountryWriteLockService;
import com.red.circle.other.infra.database.mongo.entity.team.bd.BdInviteAgentMessage;
import com.red.circle.other.infra.database.mongo.entity.team.bd.MessageInviteGroup; import com.red.circle.other.infra.database.mongo.entity.team.bd.MessageInviteGroup;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; 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.entity.team.team.TeamProfile;
import com.red.circle.other.infra.database.mongo.service.team.bd.BdInviteAgentMessageService; import com.red.circle.other.infra.database.mongo.service.team.bd.BdInviteAgentMessageService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamBillCycleService; import com.red.circle.other.infra.database.mongo.service.team.team.TeamBillCycleService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService; 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.mongo.service.team.team.TeamProfileService;
import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentBaseInfo; import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentTeam; 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.entity.team.BusinessDevelopmentTeam;
import com.red.circle.other.infra.database.rds.service.sys.AdministratorService;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentBaseInfoService; import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentBaseInfoService;
import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentTeamService; import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentTeamService;
import com.red.circle.other.infra.database.rds.service.team.UserHistoryIdentityService; import com.red.circle.other.infra.database.rds.service.team.UserHistoryIdentityService;
@ -49,8 +53,9 @@ import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils; import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -76,8 +81,11 @@ public class BdInviteMessageProcessExe {
private final BusinessDevelopmentTeamService businessDevelopmentTeamService; private final BusinessDevelopmentTeamService businessDevelopmentTeamService;
private final BusinessDevelopmentBaseInfoService businessDevelopmentBaseInfoService; private final BusinessDevelopmentBaseInfoService businessDevelopmentBaseInfoService;
private final UserMqMessageService userMqMessageService; private final UserMqMessageService userMqMessageService;
private final AdministratorService administratorService; private final AdministratorService administratorService;
private final TaskMqMessage taskMqMessage; private final TaskMqMessage taskMqMessage;
private final SysCountryCodeService sysCountryCodeService;
private final CountryDataSyncLockService countryDataSyncLockService;
private final UserCountryWriteLockService userCountryWriteLockService;
public void execute(DbInviteMessageProcessCmd cmd) { public void execute(DbInviteMessageProcessCmd cmd) {
@ -100,12 +108,8 @@ public class BdInviteMessageProcessExe {
// 6014 用户正在申请加入其他代理 // 6014 用户正在申请加入其他代理
ResponseAssert.isFalse(TeamErrorCode.USER_APPLY_JOIN_OTHER_AGENT, ResponseAssert.isFalse(TeamErrorCode.USER_APPLY_JOIN_OTHER_AGENT,
teamMemberService.existsMember(cmd.requiredReqUserId())); teamMemberService.existsMember(cmd.requiredReqUserId()));
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE, if (Objects.equals(cmd.getStatus(), 1)) {
bdInviteAgentMessageService.updateStatus(cmd.getId(), message.getStatus(), Long teamId = createTeam(cmd, message);
cmd.getStatus(), message.getGroup()));
if (Objects.equals(cmd.getStatus(), 1)) {
Long teamId = createTeam(cmd, message);
// 主播设备指纹校验 // 主播设备指纹校验
Boolean hasAnchorWithSameDevice = registerDeviceGateway.checkDeviceFingerprintHasAnchor( Boolean hasAnchorWithSameDevice = registerDeviceGateway.checkDeviceFingerprintHasAnchor(
@ -128,10 +132,13 @@ public class BdInviteMessageProcessExe {
userMqMessageService.sendBadgeOperateEvent(message.getInviteUserId(), BadgeKeyEnum.HOST.name(), BadgeOperateEvent.OperationType.WEAR.name()); userMqMessageService.sendBadgeOperateEvent(message.getInviteUserId(), BadgeKeyEnum.HOST.name(), BadgeOperateEvent.OperationType.WEAR.name());
sendMessage(message, OfficialNoticeTypeEnum.AGREE_BD_INVITE_AGENT); sendMessage(message, OfficialNoticeTypeEnum.AGREE_BD_INVITE_AGENT);
return; return;
} }
sendMessage(message, OfficialNoticeTypeEnum.REFUSE_BD_INVITE_AGENT); ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
bdInviteAgentMessageService.updateStatus(cmd.getId(), message.getStatus(),
cmd.getStatus(), message.getGroup()));
sendMessage(message, OfficialNoticeTypeEnum.REFUSE_BD_INVITE_AGENT);
} }
private void sendAdminTaskMessage(Long userId) { private void sendAdminTaskMessage(Long userId) {
@ -156,9 +163,62 @@ public class BdInviteMessageProcessExe {
message.getInviteUserId()); message.getInviteUserId());
ResponseAssert.isFalse(TeamErrorCode.DEVICE_FINGERPRINT_USED_BY_ANCHOR, hasAnchorWithSameDevice); ResponseAssert.isFalse(TeamErrorCode.DEVICE_FINGERPRINT_USED_BY_ANCHOR, hasAnchorWithSameDevice);
UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO( UserProfileDTO preflightUser = userProfileAppConvertor.toUserProfileDTO(
userProfileGateway.getByUserId(message.getInviteUserId())); userProfileGateway.getByUserId(message.getInviteUserId()));
TeamProfile teamProfile = new TeamProfile() ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, preflightUser);
Long expectedCountryId = preflightUser.getCountryId();
// 锁顺序固定为 country -> user邀请状态与团队全部初始化完成后才释放.
return executeTeamCreationLocks(expectedCountryId, message.getInviteUserId(), () ->
createTeamLocked(cmd, message, bdBaseInfo, expectedCountryId));
}
private Long createTeamLocked(DbInviteMessageProcessCmd cmd, BdInviteAgentMessage message,
BusinessDevelopmentBaseInfo bdBaseInfo, Long expectedCountryId) {
TeamCreationContext creationContext = buildTeamProfile(cmd, message, bdBaseInfo);
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT,
Objects.equals(expectedCountryId, creationContext.userProfile().getCountryId()));
// 接受状态必须在取得国家写锁后更新锁冲突时保留待处理状态供安全重试.
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
bdInviteAgentMessageService.updateStatus(cmd.getId(), message.getStatus(),
cmd.getStatus(), message.getGroup()));
teamProfileService.create(creationContext.teamProfile());
UserProfileDTO userProfile = creationContext.userProfile();
TeamProfile teamProfile = creationContext.teamProfile();
// 初始化账单
teamBillCycleService.createIfAbsent(
new TeamBillCmd()
.setSysOrigin(teamProfile.getSysOrigin())
.setTeamId(teamProfile.getId())
.setRegion(teamProfile.getRegion())
.setOperationTime(TimestampUtils.now())
.setOperationBackUser(teamProfile.getCreateUser())
);
teamMemberService.addMember(new TeamMember()
.setSortId(IdWorkerUtils.getId())
.setSysOrigin(teamProfile.getSysOrigin())
.setMemberId(userProfile.getId())
.setTeamId(teamProfile.getId())
.setRole(TeamMemberRole.OWN)
.setRemarks("")
.setCreateTime(TimestampUtils.now())
.setCreateUserOrigin(1)
.setUpdateTime(TimestampUtils.now())
.setUpdateUserOrigin(1)
.setActiveTime(TimestampUtils.now())
);
userHistoryIdentityService.saveHost(userProfile.getId());
return teamProfile.getId();
}
private TeamCreationContext buildTeamProfile(DbInviteMessageProcessCmd cmd,
BdInviteAgentMessage message, BusinessDevelopmentBaseInfo bdBaseInfo) {
UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO(
userProfileGateway.getByUserId(message.getInviteUserId()));
TeamProfile teamProfile = new TeamProfile()
.setId(IdWorkerUtils.getId()) .setId(IdWorkerUtils.getId())
.setRegion(bdBaseInfo.getRegion()) .setRegion(bdBaseInfo.getRegion())
.setSysOrigin(cmd.requireReqSysOrigin()) .setSysOrigin(cmd.requireReqSysOrigin())
@ -168,11 +228,7 @@ public class BdInviteMessageProcessExe {
.setSetting(new TeamSetting() .setSetting(new TeamSetting()
.setMaxMember(1000) .setMaxMember(1000)
) )
.setCountry(new TeamCountry() .setCountry(resolveTeamCountry(userProfile))
.setCountryId(userProfile.getCountryId())
.setCountryCode(userProfile.getCountryCode())
.setCountryName(userProfile.getCountryName())
)
.setCounter(new TeamCounter() .setCounter(new TeamCounter()
.setMemberQuantity(1L) .setMemberQuantity(1L)
.setAdminQuantity(0L) .setAdminQuantity(0L)
@ -192,39 +248,36 @@ public class BdInviteMessageProcessExe {
.setCreateTime(TimestampUtils.now()) .setCreateTime(TimestampUtils.now())
.setUpdateTime(TimestampUtils.now()) .setUpdateTime(TimestampUtils.now())
.setCreateUser(cmd.requiredReqUserId()) .setCreateUser(cmd.requiredReqUserId())
.setCreateUserOrigin(OperationUserOrigin.APP.getValue()) .setCreateUserOrigin(OperationUserOrigin.APP.getValue())
.setUpdateUser(cmd.requiredReqUserId()) .setUpdateUser(cmd.requiredReqUserId())
.setUpdateUserOrigin(OperationUserOrigin.APP.getValue()); .setUpdateUserOrigin(OperationUserOrigin.APP.getValue());
teamProfileService.create(teamProfile); return new TeamCreationContext(userProfile, teamProfile);
}
// 初始化账单
teamBillCycleService.createIfAbsent( private TeamCountry resolveTeamCountry(UserProfileDTO userProfile) {
new TeamBillCmd() if (userProfile.getCountryId() == null) {
.setSysOrigin(teamProfile.getSysOrigin()) return new TeamCountry()
.setTeamId(teamProfile.getId()) .setCountryCode(userProfile.getCountryCode())
.setRegion(teamProfile.getRegion()) .setCountryName(userProfile.getCountryName());
.setOperationTime(TimestampUtils.now()) }
.setOperationBackUser(teamProfile.getCreateUser()) // countryId 是稳定归属团队冗余码与名称始终取国家主表的最新标准值.
); SysCountryCode country = sysCountryCodeService.getById(userProfile.getCountryId());
ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, country);
teamMemberService.addMember(new TeamMember() return new TeamCountry()
.setSortId(IdWorkerUtils.getId()) .setCountryId(country.getId())
.setSysOrigin(teamProfile.getSysOrigin()) .setCountryCode(country.getAlphaTwo())
.setMemberId(userProfile.getId()) .setCountryName(country.getCountryName());
.setTeamId(teamProfile.getId()) }
.setRole(TeamMemberRole.OWN)
.setRemarks("") private <T> T executeTeamCreationLocks(Long countryId, Long ownUserId, Supplier<T> action) {
.setCreateTime(TimestampUtils.now()) Supplier<T> userLocked = () -> userCountryWriteLockService.execute(
.setCreateUserOrigin(1) ownUserId == null ? List.of() : List.of(ownUserId), action);
.setUpdateTime(TimestampUtils.now()) return countryId == null ? userLocked.get()
.setUpdateUserOrigin(1) : countryDataSyncLockService.execute(countryId, userLocked);
.setActiveTime(TimestampUtils.now()) }
);
private record TeamCreationContext(UserProfileDTO userProfile, TeamProfile teamProfile) {
userHistoryIdentityService.saveHost(userProfile.getId()); }
return teamProfile.getId();
}
private void sendMessage(BdInviteAgentMessage message, OfficialNoticeTypeEnum noticeTypeEnum) { private void sendMessage(BdInviteAgentMessage message, OfficialNoticeTypeEnum noticeTypeEnum) {
UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO( UserProfileDTO userProfile = userProfileAppConvertor.toUserProfileDTO(

View File

@ -13,9 +13,11 @@ 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.dto.cmd.user.user.UserProfileModifyCmd;
import com.red.circle.other.domain.gateway.approval.ProfileApprovalGateway; 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.UserProfileGateway;
import com.red.circle.other.domain.model.approval.ProfileApprovalContent; import com.red.circle.other.domain.model.approval.ProfileApprovalContent;
import com.red.circle.other.domain.model.user.UserProfile; 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.common.sys.CountryDataSyncLockService;
import com.red.circle.other.infra.common.user.UserCountryWriteLockService;
import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService; 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.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo; import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo;
@ -57,11 +59,33 @@ public class UpdateUserProfileCmdExe {
private final SysCountryCodeService sysCountryCodeService; private final SysCountryCodeService sysCountryCodeService;
private final ProfileApprovalGateway profileApprovalGateway; private final ProfileApprovalGateway profileApprovalGateway;
private final UserProfileAppConvertor userProfileAppConvertor; private final UserProfileAppConvertor userProfileAppConvertor;
private final RoomProfileManagerService roomProfileManagerService; private final RoomProfileManagerService roomProfileManagerService;
private final CpRelationshipCacheService cpRelationshipCacheService; private final CpRelationshipCacheService cpRelationshipCacheService;
private final CountryDataSyncLockService countryDataSyncLockService;
public UserProfileDTO execute(UserProfileModifyCmd cmd) { private final UserCountryWriteLockService userCountryWriteLockService;
//敏感词
public UserProfileDTO execute(UserProfileModifyCmd cmd) {
if (cmd.getCountryId() == null) {
return executeInternal(cmd);
}
BaseInfo preflight = baseInfoService.getById(cmd.requiredReqUserId());
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, preflight);
Long sourceCountryId = preflight.getCountryId();
// 跨国修改按 country -> user 持锁避免用户在目标国家游标扫过后迁入而漏掉运行投影.
return countryDataSyncLockService.execute(
Arrays.asList(sourceCountryId, cmd.getCountryId()),
() -> userCountryWriteLockService.execute(
Collections.singletonList(cmd.requiredReqUserId()), () -> {
BaseInfo lockedUser = baseInfoService.getById(cmd.requiredReqUserId());
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, lockedUser);
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT,
Objects.equals(sourceCountryId, lockedUser.getCountryId()));
return executeInternal(cmd);
}));
}
private UserProfileDTO executeInternal(UserProfileModifyCmd cmd) {
//敏感词
if(ObjectUtil.isNotEmpty(cmd.getUserNickname())){ if(ObjectUtil.isNotEmpty(cmd.getUserNickname())){
ResponseAssert.isFalse(DynamicErrorCode.SENSITIVE_WORD_ERROR, SensitiveWordFilter.checkSensitiveWord(cmd.getUserNickname())); ResponseAssert.isFalse(DynamicErrorCode.SENSITIVE_WORD_ERROR, SensitiveWordFilter.checkSensitiveWord(cmd.getUserNickname()));
} }

View File

@ -117,19 +117,22 @@ public class UserViolationAdjustCmdExe {
default: default:
return null; return null;
} }
} }
private void updateUserProfile(UserProfile userProfile, ViolationTypeEnum violationType, String adjustedContent) { private void updateUserProfile(UserProfile userProfile, ViolationTypeEnum violationType, String adjustedContent) {
switch (violationType) { // 违规处理只提交实际变更字段避免把此前读取的完整用户国家快照重新写回主表.
case USER_NICKNAME: UserProfile updateProfile = new UserProfile();
userProfile.setUserNickname(adjustedContent); updateProfile.setId(userProfile.getId());
break; switch (violationType) {
case USER_AVATAR: case USER_NICKNAME:
userProfile.setUserAvatar(adjustedContent); updateProfile.setUserNickname(adjustedContent);
break; break;
} case USER_AVATAR:
userProfileGateway.updateSelectiveById(userProfile); updateProfile.setUserAvatar(adjustedContent);
} break;
}
userProfileGateway.updateSelectiveById(updateProfile);
}
private void sendAdjustMessage(Long userId, ViolationTypeEnum violationType) { private void sendAdjustMessage(Long userId, ViolationTypeEnum violationType) {
String content = buildAdjustContent(userId, violationType); String content = buildAdjustContent(userId, violationType);

View File

@ -154,8 +154,8 @@ public class TeamBillSettleListener implements MessageListener {
List<TeamMemberTarget> teamMemberTargets = getTeamMemberTargets(teamBillCycle); List<TeamMemberTarget> teamMemberTargets = getTeamMemberTargets(teamBillCycle);
log.warn("团队账单结算teamMemberTargets."+teamMemberTargets); log.warn("团队账单结算teamMemberTargets."+teamMemberTargets);
// 获得区域配置细信息 // 历史待结算账单必须全程使用账单所属区域避免团队迁区后混用新区域汇率与旧政策.
SysRegionConfig regionConfig = sysRegionConfigService.getById(teamProfile.getRegion()); SysRegionConfig regionConfig = sysRegionConfigService.getById(teamBillCycle.getRegion());
if (Objects.isNull(regionConfig)) { if (Objects.isNull(regionConfig)) {
log.warn("没有获得团队的区域数据,团队ID:{}", teamProfile.getId()); log.warn("没有获得团队的区域数据,团队ID:{}", teamProfile.getId());
teamBillCycleService.updateStatus(teamBillCycle.getId(), TeamBillCycleStatus.HANG_UP, teamBillCycleService.updateStatus(teamBillCycle.getId(), TeamBillCycleStatus.HANG_UP,

View File

@ -14,12 +14,15 @@ import com.red.circle.other.app.dto.cmd.team.DbInviteMessageProcessCmd;
import com.red.circle.other.domain.gateway.user.UserProfileGateway; 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.gateway.user.ability.RegisterDeviceGateway;
import com.red.circle.other.domain.model.user.UserProfile; import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.common.sys.CountryDataSyncLockService;
import com.red.circle.other.infra.common.user.UserCountryWriteLockService;
import com.red.circle.other.infra.database.mongo.entity.team.bd.BdInviteAgentMessage; import com.red.circle.other.infra.database.mongo.entity.team.bd.BdInviteAgentMessage;
import com.red.circle.other.infra.database.mongo.service.team.bd.BdInviteAgentMessageService; import com.red.circle.other.infra.database.mongo.service.team.bd.BdInviteAgentMessageService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamBillCycleService; import com.red.circle.other.infra.database.mongo.service.team.team.TeamBillCycleService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService; 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.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.sys.AdministratorService;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentBaseInfoService; import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentBaseInfoService;
import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentTeamService; import com.red.circle.other.infra.database.rds.service.team.BusinessDevelopmentTeamService;
import com.red.circle.other.infra.database.rds.service.team.UserHistoryIdentityService; import com.red.circle.other.infra.database.rds.service.team.UserHistoryIdentityService;
@ -45,6 +48,11 @@ class BdInviteMessageProcessExeTest {
UserMqMessageService userMqMessageService = mock(UserMqMessageService.class); UserMqMessageService userMqMessageService = mock(UserMqMessageService.class);
AdministratorService administratorService = mock(AdministratorService.class); AdministratorService administratorService = mock(AdministratorService.class);
TaskMqMessage taskMqMessage = mock(TaskMqMessage.class); TaskMqMessage taskMqMessage = mock(TaskMqMessage.class);
SysCountryCodeService sysCountryCodeService = mock(SysCountryCodeService.class);
CountryDataSyncLockService countryDataSyncLockService =
mock(CountryDataSyncLockService.class);
UserCountryWriteLockService userCountryWriteLockService =
mock(UserCountryWriteLockService.class);
BdInviteMessageProcessExe exe = new BdInviteMessageProcessExe( BdInviteMessageProcessExe exe = new BdInviteMessageProcessExe(
teamMemberService, teamMemberService,
@ -59,7 +67,10 @@ class BdInviteMessageProcessExeTest {
businessDevelopmentBaseInfoService, businessDevelopmentBaseInfoService,
userMqMessageService, userMqMessageService,
administratorService, administratorService,
taskMqMessage taskMqMessage,
sysCountryCodeService,
countryDataSyncLockService,
userCountryWriteLockService
); );
DbInviteMessageProcessCmd cmd = new DbInviteMessageProcessCmd(); DbInviteMessageProcessCmd cmd = new DbInviteMessageProcessCmd();

View File

@ -1,7 +1,10 @@
package com.red.circle.other.app.command.user; package com.red.circle.other.app.command.user;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -13,9 +16,12 @@ import com.red.circle.other.app.dto.cmd.user.user.UserProfileModifyCmd;
import com.red.circle.other.domain.gateway.approval.ProfileApprovalGateway; 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.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile; import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.common.sys.CountryDataSyncLockService;
import com.red.circle.other.infra.common.user.UserCountryWriteLockService;
import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService; import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService; 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.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService; import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
import com.red.circle.other.infra.database.rds.service.user.user.BaseInfoService; import com.red.circle.other.infra.database.rds.service.user.user.BaseInfoService;
import com.red.circle.other.inner.model.dto.user.PhotoItem; import com.red.circle.other.inner.model.dto.user.PhotoItem;
@ -23,6 +29,7 @@ import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.component.redis.service.RedisService; import com.red.circle.component.redis.service.RedisService;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@ -39,6 +46,12 @@ class UpdateUserProfileCmdExeTest {
UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class); UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class);
RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class); RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class);
CpRelationshipCacheService cpRelationshipCacheService = mock(CpRelationshipCacheService.class); CpRelationshipCacheService cpRelationshipCacheService = mock(CpRelationshipCacheService.class);
CountryDataSyncLockService countryDataSyncLockService =
new CountryDataSyncLockService(redisService);
UserCountryWriteLockService userCountryWriteLockService =
new UserCountryWriteLockService(redisService);
when(redisService.setIfAbsent(anyString(), any(), anyLong(), any(TimeUnit.class)))
.thenReturn(true);
UpdateUserProfileCmdExe exe = new UpdateUserProfileCmdExe( UpdateUserProfileCmdExe exe = new UpdateUserProfileCmdExe(
ossServiceClient, ossServiceClient,
userProfileGateway, userProfileGateway,
@ -48,7 +61,9 @@ class UpdateUserProfileCmdExeTest {
profileApprovalGateway, profileApprovalGateway,
userProfileAppConvertor, userProfileAppConvertor,
roomProfileManagerService, roomProfileManagerService,
cpRelationshipCacheService cpRelationshipCacheService,
countryDataSyncLockService,
userCountryWriteLockService
); );
UserProfileModifyCmd cmd = new UserProfileModifyCmd(); UserProfileModifyCmd cmd = new UserProfileModifyCmd();
@ -68,6 +83,7 @@ class UpdateUserProfileCmdExeTest {
.setCountryName("Japan"); .setCountryName("Japan");
when(userProfileGateway.getByUserId(1L)).thenReturn(currentUserProfile); when(userProfileGateway.getByUserId(1L)).thenReturn(currentUserProfile);
when(baseInfoService.getById(1L)).thenReturn(new BaseInfo().setId(1L).setCountryId(1L));
when(userProfileAppConvertor.toUserProfile(cmd)).thenReturn(updateUserProfile); when(userProfileAppConvertor.toUserProfile(cmd)).thenReturn(updateUserProfile);
when(sysCountryCodeService.getById(2L)).thenReturn(newCountry); when(sysCountryCodeService.getById(2L)).thenReturn(newCountry);
when(userProfileAppConvertor.toUserProfileDTO(currentUserProfile)).thenReturn(new UserProfileDTO()); when(userProfileAppConvertor.toUserProfileDTO(currentUserProfile)).thenReturn(new UserProfileDTO());
@ -94,6 +110,10 @@ class UpdateUserProfileCmdExeTest {
UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class); UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class);
RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class); RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class);
CpRelationshipCacheService cpRelationshipCacheService = mock(CpRelationshipCacheService.class); CpRelationshipCacheService cpRelationshipCacheService = mock(CpRelationshipCacheService.class);
CountryDataSyncLockService countryDataSyncLockService =
new CountryDataSyncLockService(redisService);
UserCountryWriteLockService userCountryWriteLockService =
new UserCountryWriteLockService(redisService);
UpdateUserProfileCmdExe exe = new UpdateUserProfileCmdExe( UpdateUserProfileCmdExe exe = new UpdateUserProfileCmdExe(
ossServiceClient, ossServiceClient,
userProfileGateway, userProfileGateway,
@ -103,7 +123,9 @@ class UpdateUserProfileCmdExeTest {
profileApprovalGateway, profileApprovalGateway,
userProfileAppConvertor, userProfileAppConvertor,
roomProfileManagerService, roomProfileManagerService,
cpRelationshipCacheService cpRelationshipCacheService,
countryDataSyncLockService,
userCountryWriteLockService
); );
String backgroundUrl = "https://example.com/profile-bg.jpg"; String backgroundUrl = "https://example.com/profile-bg.jpg";

View File

@ -0,0 +1,78 @@
package com.red.circle.other.infra.common.sys;
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 java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 国家资料级联全局写锁防止同一国家同时更新或创建新团队.
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class CountryDataSyncLockService {
private static final int LOCK_SECONDS = 600;
private static final String LOCK_PREFIX = "sys_country_code_sync_lock:v2:";
private static final String RELEASE_LOCK_SCRIPT =
"if redis.call('GET', KEYS[1]) == ARGV[1] then "
+ "return redis.call('DEL', KEYS[1]) "
+ "end "
+ "return 0";
private final RedisService redisService;
/**
* 以唯一 token 持有国家锁超时后不会误删新持有者的锁.
*/
public <T> T execute(Long countryId, Supplier<T> action) {
return execute(List.of(countryId), action);
}
/**
* 按国家ID排序持有多把锁用于用户跨国迁移同时保护原国家与目标国家.
*/
public <T> T execute(Collection<Long> countryIds, Supplier<T> action) {
Map<String, String> lockedKeys = new LinkedHashMap<>();
try {
countryIds.stream().filter(Objects::nonNull).distinct().sorted(Comparator.naturalOrder())
.forEach(countryId -> {
String key = key(countryId);
String token = UUID.randomUUID().toString();
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT,
redisService.setIfAbsent(key, token, LOCK_SECONDS, TimeUnit.SECONDS));
lockedKeys.put(key, token);
});
return action.get();
} finally {
List<Map.Entry<String, String>> entries = new ArrayList<>(lockedKeys.entrySet());
for (int index = entries.size() - 1; index >= 0; index--) {
try {
Map.Entry<String, String> entry = entries.get(index);
redisService.execute(RELEASE_LOCK_SCRIPT, Long.class, List.of(entry.getKey()),
entry.getValue());
} catch (RuntimeException ex) {
// 解锁故障不应覆盖已成功的业务结果token锁会在TTL后自动释放.
log.warn("释放国家资料级联写锁失败", ex);
}
}
}
}
private String key(Long countryId) {
return LOCK_PREFIX + countryId;
}
}

View File

@ -15,10 +15,12 @@ import com.red.circle.framework.web.util.ValidatorUtils;
import com.red.circle.mq.business.model.event.task.TaskApprovalEvent; import com.red.circle.mq.business.model.event.task.TaskApprovalEvent;
import com.red.circle.mq.rocket.business.producer.TaskMqMessage; import com.red.circle.mq.rocket.business.producer.TaskMqMessage;
import com.red.circle.other.domain.gateway.user.UserAccountShortProductionGateway; import com.red.circle.other.domain.gateway.user.UserAccountShortProductionGateway;
import com.red.circle.other.domain.gateway.user.UserProfileGateway; import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile; import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.convertor.user.UserProfileInfraConvertor; import com.red.circle.other.infra.common.sys.CountryDataSyncLockService;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserRunProfileService; import com.red.circle.other.infra.convertor.user.UserProfileInfraConvertor;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserRunProfileService;
import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.entity.user.user.ApprovalUserAccountStatusLog; import com.red.circle.other.infra.database.rds.entity.user.user.ApprovalUserAccountStatusLog;
import com.red.circle.other.infra.database.rds.entity.user.user.AuthType; import com.red.circle.other.infra.database.rds.entity.user.user.AuthType;
import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo; import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo;
@ -26,7 +28,8 @@ import com.red.circle.other.infra.database.rds.entity.user.user.MobileAuth;
import com.red.circle.other.infra.database.rds.entity.user.user.RegisterInfo; import com.red.circle.other.infra.database.rds.entity.user.user.RegisterInfo;
import com.red.circle.other.infra.database.rds.entity.user.user.UserInviteUser; import com.red.circle.other.infra.database.rds.entity.user.user.UserInviteUser;
import com.red.circle.other.infra.database.rds.service.user.device.ArchiveDeviceService; import com.red.circle.other.infra.database.rds.service.user.device.ArchiveDeviceService;
import com.red.circle.other.infra.database.rds.service.user.device.LatestMobileDeviceService; import com.red.circle.other.infra.database.rds.service.user.device.LatestMobileDeviceService;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
import com.red.circle.other.infra.database.rds.service.user.user.ApprovalUserAccountStatusLogService; import com.red.circle.other.infra.database.rds.service.user.user.ApprovalUserAccountStatusLogService;
import com.red.circle.other.infra.database.rds.service.user.user.AuthTypeService; import com.red.circle.other.infra.database.rds.service.user.user.AuthTypeService;
import com.red.circle.other.infra.database.rds.service.user.user.BaseInfoService; import com.red.circle.other.infra.database.rds.service.user.user.BaseInfoService;
@ -67,8 +70,10 @@ import org.springframework.transaction.interceptor.TransactionAspectSupport;
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserAccountCommon { public class UserAccountCommon {
private final AuthClient authClient; private final AuthClient authClient;
private final BaseInfoService baseInfoService; private final BaseInfoService baseInfoService;
private final SysCountryCodeService sysCountryCodeService;
private final CountryDataSyncLockService countryDataSyncLockService;
private final AuthTypeService authTypeService; private final AuthTypeService authTypeService;
private final ImAccountClient imAccountClient; private final ImAccountClient imAccountClient;
private final NewsletterClient newsletterClient; private final NewsletterClient newsletterClient;
@ -93,14 +98,11 @@ public class UserAccountCommon {
BaseInfo baseInfo = toBaseInfo(cmd); BaseInfo baseInfo = toBaseInfo(cmd);
String userSig = requestUserSign(baseInfo.getId()); String userSig = requestUserSign(baseInfo.getId());
try { try {
// 客户端时区判断问题出现了新用户注册就被冻结现象 // 客户端时区判断问题出现了新用户注册就被冻结现象
baseInfo.setFreezingTime(TimestampUtils.convert(LocalDateTime.now().minusDays(1))); baseInfo.setFreezingTime(TimestampUtils.convert(LocalDateTime.now().minusDays(1)));
mobileRegister(cmd, baseInfo); UserProfile inviteUser = inviteRegister(cmd, baseInfo);
UserProfile inviteUser = inviteRegister(cmd, baseInfo); saveAccountRecordsWithCanonicalCountry(cmd, baseInfo);
baseInfoService.save(baseInfo); // saveUserInviteData(baseInfo.getId(), cmd, inviteUser);
registerInfoService.save(toRegisterInfo(baseInfo.getId(), cmd));
authTypeService.save(toAuthType(baseInfo.getId(), cmd));
// saveUserInviteData(baseInfo.getId(), cmd, inviteUser);
} catch (DuplicateKeyException ex) { } catch (DuplicateKeyException ex) {
log.error("RegisterAddCmdExe:{},{}", ex, JacksonUtils.toJson(cmd)); log.error("RegisterAddCmdExe:{},{}", ex, JacksonUtils.toJson(cmd));
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
@ -109,8 +111,33 @@ public class UserAccountCommon {
return new UserAccountDTO() return new UserAccountDTO()
.setUserProfile(userProfileInfraConvertor.toUserProfileDTO(baseInfo)) .setUserProfile(userProfileInfraConvertor.toUserProfileDTO(baseInfo))
.setUserSig(userSig); .setUserSig(userSig);
} }
private void saveAccountRecordsWithCanonicalCountry(CreateAccountCmd cmd, BaseInfo baseInfo) {
if (baseInfo.getCountryId() == null) {
saveAccountRecords(cmd, baseInfo);
return;
}
// 邀请注册可能在前置处理后改变最终国家落库前按最终 countryId 持锁并重读主表
// 避免国家级联完成后才写入旧国家码的注册竞态.
countryDataSyncLockService.execute(baseInfo.getCountryId(), () -> {
SysCountryCode country = sysCountryCodeService.getById(baseInfo.getCountryId());
ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR, country);
baseInfo.setCountryCode(country.getAlphaTwo());
baseInfo.setCountryName(country.getCountryName());
saveAccountRecords(cmd, baseInfo);
return null;
});
}
private void saveAccountRecords(CreateAccountCmd cmd, BaseInfo baseInfo) {
// 账号核心记录在国家锁内连续写入锁冲突时不会遗留先写入的手机号认证记录.
mobileRegister(cmd, baseInfo);
baseInfoService.save(baseInfo);
registerInfoService.save(toRegisterInfo(baseInfo.getId(), cmd));
authTypeService.save(toAuthType(baseInfo.getId(), cmd));
}
/** /**
* 处理邀请注册信息 * 处理邀请注册信息

View File

@ -0,0 +1,67 @@
package com.red.circle.other.infra.common.user;
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 java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 用户国家资料写锁统一串行化单用户改国家与国家配置级联同步.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserCountryWriteLockService {
private static final int LOCK_SECONDS = 120;
private static final String LOCK_PREFIX = "user_country_write_lock:v2:";
private static final String RELEASE_LOCK_SCRIPT =
"if redis.call('GET', KEYS[1]) == ARGV[1] then "
+ "return redis.call('DEL', KEYS[1]) "
+ "end "
+ "return 0";
private final RedisService redisService;
/**
* 按用户ID排序加锁避免多个批处理交叉申请时形成相反的锁顺序.
*/
public <T> T execute(Collection<Long> userIds, Supplier<T> action) {
Map<String, String> lockedKeys = new LinkedHashMap<>();
try {
userIds.stream().filter(Objects::nonNull).distinct().sorted(Comparator.naturalOrder())
.forEach(userId -> {
String key = LOCK_PREFIX + userId;
String token = UUID.randomUUID().toString();
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT,
redisService.setIfAbsent(key, token, LOCK_SECONDS, TimeUnit.SECONDS));
lockedKeys.put(key, token);
});
return action.get();
} finally {
List<Map.Entry<String, String>> entries = new ArrayList<>(lockedKeys.entrySet());
for (int index = entries.size() - 1; index >= 0; index--) {
try {
Map.Entry<String, String> entry = entries.get(index);
// token 校验防止旧持有者超时后删掉新持有者的锁.
redisService.execute(RELEASE_LOCK_SCRIPT, Long.class, List.of(entry.getKey()),
entry.getValue());
} catch (RuntimeException ex) {
log.warn("释放用户国家资料写锁失败", ex);
}
}
}
}
}

View File

@ -1,4 +1,6 @@
package com.red.circle.other.infra.database.cache.service.user; package com.red.circle.other.infra.database.cache.service.user;
import java.util.Collection;
/** /**
@ -13,7 +15,12 @@ public interface UserRunProfileCacheService {
* *
* @param userId 用户id * @param userId 用户id
*/ */
void remove(Long userId); void remove(Long userId);
/**
* 批量移除用户运行资料缓存.
*/
void remove(Collection<Long> userIds);
/** /**
* 添加缓存. * 添加缓存.

View File

@ -30,9 +30,17 @@ public class UserRunProfileCacheServiceImpl implements UserRunProfileCacheServic
private static final long CACHE_TIME = 60; private static final long CACHE_TIME = 60;
@Override @Override
public void remove(Long userId) { public void remove(Long userId) {
redisService.delete(getUserRunProfileKey(userId)); redisService.delete(getUserRunProfileKey(userId));
} }
@Override
public void remove(Collection<Long> userIds) {
if (CollectionUtils.isEmpty(userIds)) {
return;
}
redisService.delete(userIds.stream().map(this::getUserRunProfileKey).toList());
}
@Override @Override
public void save(String json, Long userId) { public void save(String json, Long userId) {

View File

@ -9,8 +9,9 @@ import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
/** /**
* 代理活动. * 代理活动.
@ -18,9 +19,10 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @author pengshigang on 2023/5/25 * @author pengshigang on 2023/5/25
*/ */
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@Document("agent_activity_count") @Document("agent_activity_count")
@FieldDefaults(level = AccessLevel.PRIVATE) @CompoundIndex(name = "idx_team_history", def = "{'teamId': 1, 'history': 1}")
@FieldDefaults(level = AccessLevel.PRIVATE)
public class AgentActivityCount implements Serializable { public class AgentActivityCount implements Serializable {
@Serial @Serial

View File

@ -11,8 +11,9 @@ import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
/** /**
* 活跃语音房间. * 活跃语音房间.
@ -20,9 +21,10 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @author pengliang on 2020/12/11 * @author pengliang on 2020/12/11
*/ */
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@Document("active_voice_room") @Document("active_voice_room")
@FieldDefaults(level = AccessLevel.PRIVATE) @CompoundIndex(name = "idx_user_id", def = "{'userId': 1}")
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ActiveVoiceRoom implements Serializable { public class ActiveVoiceRoom implements Serializable {
@Serial @Serial

View File

@ -10,8 +10,9 @@ import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
/** /**
* <p> * <p>
@ -22,9 +23,10 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @since 2022/6/24 * @since 2022/6/24
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Document("room_profile_manager") @Document("room_profile_manager")
public class RoomProfileManager extends RoomProfile implements Serializable { @CompoundIndex(name = "idx_user_id", def = "{'userId': 1}")
public class RoomProfileManager extends RoomProfile implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -14,7 +14,8 @@ import java.util.List;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex; import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Document;
/** /**
@ -25,7 +26,10 @@ import org.springframework.data.mongodb.core.mapping.Document;
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@Document("team_bill_cycle") @Document("team_bill_cycle")
@CompoundIndex(name = "idx_billBelong_status", def = "{'billBelong': 1, 'status': 1}") @CompoundIndexes({
@CompoundIndex(name = "idx_billBelong_status", def = "{'billBelong': 1, 'status': 1}"),
@CompoundIndex(name = "idx_team_status", def = "{'teamId': 1, 'status': 1}")
})
public class TeamBillCycle implements Serializable { public class TeamBillCycle implements Serializable {
@Serial @Serial

View File

@ -7,8 +7,10 @@ import java.io.Serializable;
import java.sql.Timestamp; import java.sql.Timestamp;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.mapping.Document;
/** /**
* 团队成员. * 团队成员.
@ -16,9 +18,13 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @author pengliang on 2022/12/1 * @author pengliang on 2022/12/1
*/ */
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@Document("team_member") @Document("team_member")
public class TeamMember implements Serializable { @CompoundIndexes({
@CompoundIndex(name = "idx_team_member", def = "{'teamId': 1, 'memberId': 1}"),
@CompoundIndex(name = "idx_member_team", def = "{'memberId': 1, 'teamId': 1}")
})
public class TeamMember implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -12,8 +12,10 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.mapping.Document;
/** /**
* 团队成员目标. * 团队成员目标.
@ -21,9 +23,13 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @author pengliang on 2022/12/1 * @author pengliang on 2022/12/1
*/ */
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@Document("team_member_target") @Document("team_member_target")
public class TeamMemberTarget implements Serializable { @CompoundIndexes({
@CompoundIndex(name = "idx_team_history", def = "{'teamId': 1, 'history': 1}"),
@CompoundIndex(name = "idx_user_history", def = "{'userId': 1, 'history': 1}")
})
public class TeamMemberTarget implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -15,8 +15,10 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.mapping.Document;
/** /**
* 团队资料. * 团队资料.
@ -24,9 +26,13 @@ import org.springframework.data.mongodb.core.mapping.Document;
* @author pengliang on 2022/12/1 * @author pengliang on 2022/12/1
*/ */
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@Document("team_profile") @Document("team_profile")
public class TeamProfile implements Serializable { @CompoundIndexes({
@CompoundIndex(name = "idx_country_id_cursor",
def = "{'country.countryId': 1, '_id': 1}")
})
public class TeamProfile implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -31,7 +31,12 @@ public interface AgentActivityCountService {
/** /**
* 删除代理积分. * 删除代理积分.
*/ */
void removeByTeamIds(Collection<Long> teamIds); void removeByTeamIds(Collection<Long> teamIds);
/**
* 团队迁区时同步当前活动统计的区域保留已累计积分.
*/
void updateCurrentRegion(Long teamId, String regionId);
/** /**
* 修改状态为历史top用户. * 修改状态为历史top用户.

View File

@ -117,7 +117,7 @@ public class AgentActivityCountServiceImpl implements AgentActivityCountService
} }
@Override @Override
public void reduceTarget(Collection<Long> teamIds, Long target) { public void reduceTarget(Collection<Long> teamIds, Long target) {
if (CollectionUtils.isEmpty(teamIds) || target <= 0) { if (CollectionUtils.isEmpty(teamIds) || target <= 0) {
return; return;
@ -136,7 +136,17 @@ public class AgentActivityCountServiceImpl implements AgentActivityCountService
new Update().set("target", count.getTarget() > target ? count.getTarget() - target : 0), new Update().set("target", count.getTarget() > target ? count.getTarget() - target : 0),
AgentActivityCount.class)); AgentActivityCount.class));
} }
@Override
public void updateCurrentRegion(Long teamId, String regionId) {
if (teamId == null || StringUtils.isBlank(regionId)) {
return;
}
mongoTemplate.updateMulti(Query.query(Criteria.where("teamId").is(teamId)
.and("history").ne(Boolean.TRUE)),
new Update().set("regionId", regionId), AgentActivityCount.class);
}
@Override @Override
public void removeByTeamIds(Collection<Long> teamIds) { public void removeByTeamIds(Collection<Long> teamIds) {

View File

@ -3,7 +3,8 @@ package com.red.circle.other.infra.database.mongo.service.live;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom; import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
import com.red.circle.other.inner.model.cmd.live.AppActiveVoiceRoomQryCmd; import com.red.circle.other.inner.model.cmd.live.AppActiveVoiceRoomQryCmd;
import java.util.List; import java.util.List;
import java.util.Collection;
/** /**
* 活跃语音房间. * 活跃语音房间.
@ -41,7 +42,22 @@ public interface ActiveVoiceRoomService {
* @param roomAccount 房间账号 * @param roomAccount 房间账号
* @param quantity 数量 * @param quantity 数量
*/ */
void updateQuantityByRoomAccount(String roomAccount, Long quantity); void updateQuantityByRoomAccount(String roomAccount, Long quantity);
/**
* 批量同步当前活跃房间中的用户国家快照.
*/
void updateUserCountry(Collection<Long> userIds, String countryCode, String countryName);
/**
* 查询一批房主当前的活跃房间供国家同步精确失效发现缓存.
*/
List<ActiveVoiceRoom> listByUserIds(Collection<Long> userIds);
/**
* 更新房主的默认区域存在生效中定向置顶配置的房间保持其固定区域不变.
*/
void updateUserDefaultRegion(Collection<Long> userIds, String regionCode);
/** /**
* 移除首页房间. * 移除首页房间.

View File

@ -6,7 +6,8 @@ import com.red.circle.other.infra.database.mongo.entity.live.RoomProfileManager;
import com.red.circle.other.infra.database.mongo.entity.live.RoomSetting; import com.red.circle.other.infra.database.mongo.entity.live.RoomSetting;
import com.red.circle.other.inner.model.cmd.room.RoomProfileOpsQryCmd; import com.red.circle.other.inner.model.cmd.room.RoomProfileOpsQryCmd;
import com.red.circle.other.inner.model.dto.material.UseBadgeDTO; import com.red.circle.other.inner.model.dto.material.UseBadgeDTO;
import java.util.List; import java.util.List;
import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -220,7 +221,12 @@ public interface RoomProfileManagerService {
/** /**
* 修改房间归属国家. * 修改房间归属国家.
*/ */
void updateUserCountry(Long userId, String countryCode, String countryName); void updateUserCountry(Long userId, String countryCode, String countryName);
/**
* 批量修改用户当前房间的归属国家.
*/
void updateUserCountry(Collection<Long> userIds, String countryCode, String countryName);
/** /**
* 使用语言. * 使用语言.

View File

@ -6,10 +6,12 @@ import com.red.circle.framework.mybatis.constant.PageConstant;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport; import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.mongo.dto.query.live.ActiveVoiceRoomQuery; import com.red.circle.other.infra.database.mongo.dto.query.live.ActiveVoiceRoomQuery;
import com.red.circle.other.infra.database.mongo.dto.query.live.FamilyOnlineRoomQuery; import com.red.circle.other.infra.database.mongo.dto.query.live.FamilyOnlineRoomQuery;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom; 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.infra.database.mongo.entity.live.RoomTopFixed;
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.other.inner.model.cmd.live.AppActiveVoiceRoomQryCmd;
import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import java.util.*; import java.util.*;
@ -77,10 +79,69 @@ public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService {
} }
@Override @Override
public void updateQuantityByRoomAccount(String roomAccount, Long quantity) { public void updateQuantityByRoomAccount(String roomAccount, Long quantity) {
mongoTemplate.updateFirst(Query.query(Criteria.where("roomAccount").is(roomAccount)), mongoTemplate.updateFirst(Query.query(Criteria.where("roomAccount").is(roomAccount)),
new Update().set("onlineQuantity", quantity), ActiveVoiceRoom.class); new Update().set("onlineQuantity", quantity), ActiveVoiceRoom.class);
} }
@Override
public void updateUserCountry(Collection<Long> userIds, String countryCode, String countryName) {
if (CollectionUtils.isEmpty(userIds)) {
return;
}
mongoTemplate.updateMulti(Query.query(Criteria.where("userId").in(userIds)),
new Update()
.set("countryCode", countryCode)
.set("countryName", countryName),
ActiveVoiceRoom.class);
}
@Override
public List<ActiveVoiceRoom> listByUserIds(Collection<Long> userIds) {
if (CollectionUtils.isEmpty(userIds)) {
return CollectionUtils.newArrayList();
}
return mongoTemplate.find(Query.query(Criteria.where("userId").in(userIds)),
ActiveVoiceRoom.class);
}
@Override
public void updateUserDefaultRegion(Collection<Long> userIds, String regionCode) {
if (CollectionUtils.isEmpty(userIds) || StringUtils.isBlank(regionCode)) {
return;
}
Query activeRoomQuery = Query.query(Criteria.where("userId").in(userIds));
activeRoomQuery.fields().include("id");
Set<Long> activeRoomIds = mongoTemplate.find(activeRoomQuery, ActiveVoiceRoom.class).stream()
.map(ActiveVoiceRoom::getId)
.filter(Objects::nonNull)
.collect(java.util.stream.Collectors.toSet());
if (CollectionUtils.isEmpty(activeRoomIds)) {
return;
}
// 定向置顶房间的区域由 room_top_fixed 控制国家或团队变化不能覆盖该运营配置.
java.sql.Timestamp now = TimestampUtils.now();
Criteria effectiveFixed = new Criteria().andOperator(
Criteria.where("id").in(activeRoomIds),
Criteria.where("startTime").lte(now),
new Criteria().orOperator(
Criteria.where("expiredTime").is(null),
Criteria.where("expiredTime").gt(now)));
Set<Long> fixedRegionRoomIds = mongoTemplate.find(Query.query(effectiveFixed),
RoomTopFixed.class).stream()
.filter(fixed -> StringUtils.isNotBlank(fixed.getRegion()))
.map(RoomTopFixed::getId)
.collect(java.util.stream.Collectors.toSet());
Criteria updateCriteria = Criteria.where("userId").in(userIds);
if (CollectionUtils.isNotEmpty(fixedRegionRoomIds)) {
updateCriteria.and("id").nin(fixedRegionRoomIds);
}
mongoTemplate.updateMulti(Query.query(updateCriteria),
new Update().set("region", regionCode), ActiveVoiceRoom.class);
}
@Override @Override
public void removeByIds(List<Long> ids) { public void removeByIds(List<Long> ids) {

View File

@ -24,6 +24,7 @@ import com.red.circle.tool.core.obj.ObjectUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -651,6 +652,19 @@ public class RoomProfileManagerServiceImpl implements RoomProfileManagerService
RoomProfileManager.class); RoomProfileManager.class);
} }
@Override
public void updateUserCountry(Collection<Long> userIds, String countryCode, String countryName) {
if (CollectionUtils.isEmpty(userIds)) {
return;
}
// updateMulti 不携带查询排序避免 MongoDB updateMany 拒绝 sort 参数.
mongoTemplate.updateMulti(Query.query(currentRoomCriteria().and("userId").in(userIds)),
new Update().set("updateTime", TimestampUtils.now())
.set("countryCode", countryCode)
.set("countryName", countryName),
RoomProfileManager.class);
}
private Query currentRoomQueryByUserId(Long userId) { private Query currentRoomQueryByUserId(Long userId) {
return Query.query(currentRoomCriteria().and("userId").is(userId)) return Query.query(currentRoomCriteria().and("userId").is(userId))
.with(Sort.by( .with(Sort.by(

View File

@ -130,7 +130,17 @@ public interface TeamMemberTargetService {
/** /**
* 修改本月将成员账单归类于历史账单. * 修改本月将成员账单归类于历史账单.
*/ */
void updateThisMonthUserTargetHistoryStatusByTeamId(Long teamId); void updateThisMonthUserTargetHistoryStatusByTeamId(Long teamId);
/**
* 团队跨区时只迁移当前有效目标的区域保留已累计流水和每日目标.
*/
void updateCurrentRegionByTeamId(Long teamId, String region);
/**
* 国家码修改时按用户同步当前有效目标的国家码不覆盖其他国家团队成员.
*/
void updateCurrentCountryCodeByUserIds(Collection<Long> userIds, String countryCode);
/** /**
* 获取指定目标记录. * 获取指定目标记录.

View File

@ -3,7 +3,8 @@ package com.red.circle.other.infra.database.mongo.service.team.team;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile; import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile;
import com.red.circle.other.inner.model.cmd.team.StatusTeamBatchCmd; 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.cmd.team.TeamTableQryCmd;
import com.red.circle.other.inner.model.dto.agency.agency.TeamContact; import com.red.circle.other.inner.model.dto.agency.agency.TeamContact;
import com.red.circle.other.inner.model.dto.agency.agency.TeamCountry;
import com.red.circle.other.inner.model.dto.agency.agency.TeamCounter; import com.red.circle.other.inner.model.dto.agency.agency.TeamCounter;
import com.red.circle.other.inner.model.dto.agency.agency.TeamRemark; import com.red.circle.other.inner.model.dto.agency.agency.TeamRemark;
import java.util.List; import java.util.List;
@ -121,7 +122,43 @@ public interface TeamProfileService {
/** /**
* 获取团队资料列表. * 获取团队资料列表.
*/ */
List<TeamProfile> listByIds(Set<Long> ids); List<TeamProfile> listByIds(Set<Long> ids);
/**
* 按稳定国家ID获取全部团队.
*/
List<TeamProfile> listByCountryId(Long countryId);
/**
* 按国家ID和团队ID游标分页读取避免一次加载大国家下全部团队.
*/
List<TeamProfile> listCountryTeamsAfterId(Long countryId, Long lastId, Integer limit);
/**
* 查询国家下存在团队的应用来源.
*/
Set<String> listOriginSysByCountryId(Long countryId);
/**
* 判断团队国家编码快照是否仍有未同步数据.
*/
boolean existsCountryCodeMismatch(Long countryId, String countryCode);
/**
* 判断团队国家名称快照是否仍有未同步数据.
*/
boolean existsCountryNameMismatch(Long countryId, String countryName);
/**
* 判断指定来源的团队是否仍未迁到目标区域.
*/
boolean existsCountryRegionMismatch(Long countryId, String sysOrigin, String region);
/**
* 仅当团队仍属于指定国家ID时更新国家与区域避免覆盖并发修改.
*/
boolean updateCountryAndRegion(Long teamId, Long expectedCountryId, String expectedRegion,
String region, TeamCountry country, Long updateUser);
/** /**
* 获取团队资料映射表. * 获取团队资料映射表.

View File

@ -721,9 +721,10 @@ public class TeamBillCycleServiceImpl implements TeamBillCycleService {
} }
@Override @Override
public void updateUnpaidTeamBillRegion(Long teamId, String region, Long updateUser) { public void updateUnpaidTeamBillRegion(Long teamId, String region, Long updateUser) {
mongoTemplate.updateFirst(Query.query(Criteria.where("teamId").is(teamId) // 理论上每队只有一条未出账账单updateMulti 同时修复可能存在的历史重复脏数据.
.and("status").is(TeamBillCycleStatus.UNPAID)), mongoTemplate.updateMulti(Query.query(Criteria.where("teamId").is(teamId)
.and("status").is(TeamBillCycleStatus.UNPAID)),
new Update() new Update()
.set("region", region) .set("region", region)
.set("updateUser", updateUser) .set("updateUser", updateUser)

View File

@ -82,7 +82,7 @@ public class TeamMemberTargetServiceImpl implements TeamMemberTargetService {
} }
@Override @Override
public void updateTargetCover(Long userId, Integer billBelong, TeamMemberTargetIndex target) { public void updateTargetCover(Long userId, Integer billBelong, TeamMemberTargetIndex target) {
mongoTemplate.updateFirst(getUserBillQuery(userId, billBelong), mongoTemplate.updateFirst(getUserBillQuery(userId, billBelong),
new Update().set("dailyTargets", List.of(target)) new Update().set("dailyTargets", List.of(target))
.set("updateTime", TimestampUtils.now()) .set("updateTime", TimestampUtils.now())
@ -822,6 +822,28 @@ public class TeamMemberTargetServiceImpl implements TeamMemberTargetService {
return aggregated; return aggregated;
} }
@Override
public void updateCurrentRegionByTeamId(Long teamId, String region) {
if (teamId == null || StringUtils.isBlank(region)) {
return;
}
mongoTemplate.updateMulti(Query.query(Criteria.where("teamId").is(teamId)
.and("history").ne(Boolean.TRUE)),
new Update().set("region", region).set("updateTime", TimestampUtils.now()),
TeamMemberTarget.class);
}
@Override
public void updateCurrentCountryCodeByUserIds(Collection<Long> userIds, String countryCode) {
if (CollectionUtils.isEmpty(userIds) || StringUtils.isBlank(countryCode)) {
return;
}
mongoTemplate.updateMulti(Query.query(Criteria.where("userId").in(userIds)
.and("history").ne(Boolean.TRUE)),
new Update().set("countryCode", countryCode).set("updateTime", TimestampUtils.now()),
TeamMemberTarget.class);
}
@Override @Override
public List<TeamMemberTarget> listManualSalaryTargets(String sysOrigin, String countryCode, public List<TeamMemberTarget> listManualSalaryTargets(String sysOrigin, String countryCode,
Collection<Long> userIds, Integer billBelong) { Collection<Long> userIds, Integer billBelong) {

View File

@ -12,15 +12,17 @@ import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileSe
import com.red.circle.other.inner.enums.team.TeamMemberRole; 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.StatusTeamBatchCmd;
import com.red.circle.other.inner.model.cmd.team.TeamTableQryCmd; import com.red.circle.other.inner.model.cmd.team.TeamTableQryCmd;
import com.red.circle.other.inner.model.dto.agency.agency.TeamContact; import com.red.circle.other.inner.model.dto.agency.agency.TeamContact;
import com.red.circle.other.inner.model.dto.agency.agency.TeamCounter; import com.red.circle.other.inner.model.dto.agency.agency.TeamCounter;
import com.red.circle.other.inner.model.dto.agency.agency.TeamCountry;
import com.red.circle.other.inner.model.dto.agency.agency.TeamRemark; import com.red.circle.other.inner.model.dto.agency.agency.TeamRemark;
import com.red.circle.other.inner.model.dto.agency.agency.TeamSetting; import com.red.circle.other.inner.model.dto.agency.agency.TeamSetting;
import com.red.circle.other.inner.model.dto.agency.agency.TeamStatus; 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.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils; import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import java.util.List; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -41,19 +43,19 @@ import org.springframework.stereotype.Service;
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class TeamProfileServiceImpl implements TeamProfileService { public class TeamProfileServiceImpl implements TeamProfileService {
private final MongoTemplate mongoTemplate; private final MongoTemplate mongoTemplate;
@Override @Override
public void createBatch(List<TeamProfile> teamProfiles) { public void createBatch(List<TeamProfile> teamProfiles) {
mongoTemplate.insert(teamProfiles, TeamProfile.class); mongoTemplate.insert(teamProfiles, TeamProfile.class);
} }
@Override @Override
public void create(TeamProfile teamProfile) { public void create(TeamProfile teamProfile) {
mongoTemplate.save(teamProfile); mongoTemplate.save(teamProfile);
} }
@Override @Override
public boolean existsOwnUserId(Long ownUserId) { public boolean existsOwnUserId(Long ownUserId) {
@ -348,9 +350,66 @@ public class TeamProfileServiceImpl implements TeamProfileService {
} }
@Override @Override
public List<TeamProfile> listByIds(Set<Long> ids) { public List<TeamProfile> listByIds(Set<Long> ids) {
return mongoTemplate.find(Query.query(Criteria.where("id").in(ids)), TeamProfile.class); return mongoTemplate.find(Query.query(Criteria.where("id").in(ids)), TeamProfile.class);
} }
@Override
public List<TeamProfile> listByCountryId(Long countryId) {
return mongoTemplate.find(
Query.query(Criteria.where("country.countryId").is(countryId))
.with(Sort.by(Sort.Order.asc("id"))),
TeamProfile.class);
}
@Override
public List<TeamProfile> listCountryTeamsAfterId(Long countryId, Long lastId, Integer limit) {
return mongoTemplate.find(Query.query(Criteria.where("country.countryId").is(countryId)
.and("id").gt(lastId))
.with(Sort.by(Sort.Order.asc("id"))).limit(limit), TeamProfile.class);
}
@Override
public Set<String> listOriginSysByCountryId(Long countryId) {
return new HashSet<>(mongoTemplate.findDistinct(
Query.query(Criteria.where("country.countryId").is(countryId)), "sysOrigin",
TeamProfile.class, String.class));
}
@Override
public boolean existsCountryCodeMismatch(Long countryId, String countryCode) {
return mongoTemplate.exists(Query.query(Criteria.where("country.countryId").is(countryId)
.and("country.countryCode").ne(countryCode)), TeamProfile.class);
}
@Override
public boolean existsCountryNameMismatch(Long countryId, String countryName) {
return mongoTemplate.exists(Query.query(Criteria.where("country.countryId").is(countryId)
.and("country.countryName").ne(countryName)), TeamProfile.class);
}
@Override
public boolean existsCountryRegionMismatch(Long countryId, String sysOrigin, String region) {
return mongoTemplate.exists(Query.query(Criteria.where("country.countryId").is(countryId)
.and("sysOrigin").is(sysOrigin).and("region").ne(region)), TeamProfile.class);
}
@Override
public boolean updateCountryAndRegion(Long teamId, Long expectedCountryId,
String expectedRegion, String region, TeamCountry country, Long updateUser) {
Update update = new Update()
.set("country", country)
.set("updateTime", TimestampUtils.now())
.set("updateUser", updateUser)
.set("originUpdateUser", 1);
if (StringUtils.isNotBlank(region)) {
update.set("region", region);
}
return mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(teamId)
.and("country.countryId").is(expectedCountryId)
.and("region").is(expectedRegion)), update, TeamProfile.class)
.getMatchedCount() == 1;
}
@Override @Override
public Map<Long, TeamProfile> mapProfileByIds(Set<Long> ids) { public Map<Long, TeamProfile> mapProfileByIds(Set<Long> ids) {

View File

@ -1,4 +1,6 @@
package com.red.circle.other.infra.database.mongo.service.user.active; package com.red.circle.other.infra.database.mongo.service.user.active;
import java.util.Collection;
/** /**
* 用户每日活跃日志服务. * 用户每日活跃日志服务.
@ -13,6 +15,11 @@ public interface UserDailyActiveService {
* @param registerCountryCode 注册国家code * @param registerCountryCode 注册国家code
* @param language 用户语言 * @param language 用户语言
*/ */
void record(Long userId, String sysOrigin, String registerCountryCode, String language); void record(Long userId, String sysOrigin, String registerCountryCode, String language);
} /**
* 同步用户当天已生成的活跃国家快照避免国家码修改后大屏当天仍显示旧码.
*/
void updateTodayCountryCode(Collection<Long> userIds, String countryCode);
}

View File

@ -6,9 +6,10 @@ import com.red.circle.other.infra.database.mongo.service.user.active.UserDailyAc
import com.red.circle.other.infra.utils.ZonedDateTimeUtils; import com.red.circle.other.infra.utils.ZonedDateTimeUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils; import com.red.circle.tool.core.sequence.IdWorkerUtils;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit; import java.util.Collection;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.MongoTemplate;
@ -30,7 +31,7 @@ public class UserDailyActiveServiceImpl implements UserDailyActiveService {
private final StringRedisTemplate stringRedisTemplate; private final StringRedisTemplate stringRedisTemplate;
@Override @Override
public void record(Long userId, String sysOrigin, String registerCountryCode, String language) { public void record(Long userId, String sysOrigin, String registerCountryCode, String language) {
try { try {
ZonedDateTime now = ZonedDateTimeUtils.nowAsiaShanghai(); ZonedDateTime now = ZonedDateTimeUtils.nowAsiaShanghai();
String activeDate = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); String activeDate = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
@ -67,7 +68,22 @@ public class UserDailyActiveServiceImpl implements UserDailyActiveService {
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[用户每日活跃] 记录失败 userId={}", userId, e); log.warn("[用户每日活跃] 记录失败 userId={}", userId, e);
} }
} }
} @Override
public void updateTodayCountryCode(Collection<Long> userIds, String countryCode) {
if (userIds == null || userIds.isEmpty()) {
return;
}
ZonedDateTime now = ZonedDateTimeUtils.nowAsiaShanghai();
String activeDate = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
mongoTemplate.updateMulti(
Query.query(Criteria.where("userId").in(userIds).and("activeDate").is(activeDate)),
new Update()
.set("registerCountryCode", countryCode)
.set("updateTime", Timestamp.from(now.toInstant())),
UserDailyActiveLog.class);
}
}

View File

@ -24,7 +24,18 @@ public interface UserRunProfileService {
/** /**
* 修改资料信息-为空的不做处理. * 修改资料信息-为空的不做处理.
*/ */
void updateSelectiveById(UserRunProfile userRunProfile); void updateSelectiveById(UserRunProfile userRunProfile);
/**
* 仅修改账号状态避免携带运行资料快照覆盖其他并发变更.
*/
void updateAccountStatus(Long id, String accountStatus);
/**
* 批量同步用户运行资料中的国家冗余字段.
*/
void updateCountry(Collection<Long> userIds, Long countryId, String countryCode,
String countryName);
/** /**
* 修改个性签名. * 修改个性签名.

View File

@ -56,14 +56,42 @@ public class UserRunProfileServiceImpl implements UserRunProfileService {
} }
@Override @Override
public void updateSelectiveById(UserRunProfile userRunProfile) { public void updateSelectiveById(UserRunProfile userRunProfile) {
if (Objects.isNull(userRunProfile) || Objects.isNull(userRunProfile.getId())) { if (Objects.isNull(userRunProfile) || Objects.isNull(userRunProfile.getId())) {
return; return;
} }
mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(userRunProfile.getId())), mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(userRunProfile.getId())),
getUpdateSelective(userRunProfile), getUpdateSelective(userRunProfile),
UserRunProfile.class); UserRunProfile.class);
} }
@Override
public void updateAccountStatus(Long id, String accountStatus) {
if (Objects.isNull(id) || StringUtils.isBlank(accountStatus)) {
return;
}
// 账号状态更新只能写目标字段防止旧的完整快照把国家迁移结果覆盖回去
mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(id)),
new Update()
.set("accountStatus", accountStatus)
.set("updateTime", TimestampUtils.now()),
UserRunProfile.class);
}
@Override
public void updateCountry(Collection<Long> userIds, Long countryId, String countryCode,
String countryName) {
if (CollectionUtils.isEmpty(userIds)) {
return;
}
mongoTemplate.updateMulti(Query.query(Criteria.where("id").in(userIds)),
new Update()
.set("countryId", countryId)
.set("countryCode", countryCode)
.set("countryName", countryName)
.set("updateTime", TimestampUtils.now()),
UserRunProfile.class);
}
@Override @Override
public void updateUserAutograph(Long id, String autograph) { public void updateUserAutograph(Long id, String autograph) {

View File

@ -2,9 +2,10 @@ package com.red.circle.other.infra.database.rds.dao.user.user;
import com.red.circle.framework.mybatis.dao.BaseDAO; import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo; import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo;
import com.red.circle.other.inner.model.cmd.user.UserBaseInfoPageQryCmd; import com.red.circle.other.inner.model.cmd.user.UserBaseInfoPageQryCmd;
import java.util.List; import java.util.Collection;
import java.util.List;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
/** /**
@ -15,7 +16,43 @@ import org.apache.ibatis.annotations.Param;
* @author pengliang * @author pengliang
* @since 2019-08-26 * @since 2019-08-26
*/ */
public interface BaseInfoDAO extends BaseDAO<BaseInfo> { public interface BaseInfoDAO extends BaseDAO<BaseInfo> {
/**
* 按稳定国家ID游标查询用户供国家资料批量同步使用.
*/
List<BaseInfo> listCountryUsersAfterId(@Param("countryId") Long countryId,
@Param("lastId") Long lastId, @Param("limit") Integer limit);
/**
* 查询国家下涉及的应用来源.
*/
List<String> listDistinctOriginSysByCountryId(@Param("countryId") Long countryId);
/**
* 查询国家下仍保留的用户国家码供同值保存时识别历史残留编码.
*/
List<String> listDistinctCountryCodesByCountryId(@Param("countryId") Long countryId);
/**
* 判断国家编码冗余字段是否仍有未同步数据.
*/
Boolean existsCountryCodeMismatch(@Param("countryId") Long countryId,
@Param("countryCode") String countryCode);
/**
* 判断国家名称冗余字段是否仍有未同步数据.
*/
Boolean existsCountryNameMismatch(@Param("countryId") Long countryId,
@Param("countryName") String countryName);
/**
* 按国家ID与用户ID双重约束批量更新避免覆盖并发切换国家的用户.
*/
int updateCountryByIds(@Param("countryId") Long countryId,
@Param("userIds") Collection<Long> userIds,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName);
/** /**
* 获取最大的account * 获取最大的account

View File

@ -3,9 +3,10 @@ package com.red.circle.other.infra.database.rds.dao.user.user;
import com.red.circle.framework.mybatis.dao.BaseDAO; import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.other.infra.database.rds.entity.user.user.UserExpand; import com.red.circle.other.infra.database.rds.entity.user.user.UserExpand;
import com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO; import com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.Collection;
import java.util.List;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
/** /**
@ -16,7 +17,15 @@ import org.apache.ibatis.annotations.Param;
* @author pengliang * @author pengliang
* @since 2019-10-30 * @since 2019-10-30
*/ */
public interface ExpandDAO extends BaseDAO<UserExpand> { public interface ExpandDAO extends BaseDAO<UserExpand> {
/**
* 国家码重命名时仅改同一国家ID下命中明确旧码的注册国家快照.
* 未命中旧码的跨国注册历史保持不变.
*/
int updateRegisterCountryCodeByCountry(@Param("countryId") Long countryId,
@Param("sourceCountryCodes") Collection<String> sourceCountryCodes,
@Param("newCountryCode") String newCountryCode);
/** /**
* 最新两小时在线用户分布国家 * 最新两小时在线用户分布国家
@ -24,7 +33,7 @@ public interface ExpandDAO extends BaseDAO<UserExpand> {
* @param lastActiveTime 活跃时间 * @param lastActiveTime 活跃时间
* @return list * @return list
*/ */
List<ActiveUserCountryCodeDTO> findLatestActiveUserCountryCode( List<ActiveUserCountryCodeDTO> findLatestActiveUserCountryCode(
@Param("lastActiveTime") LocalDateTime lastActiveTime); @Param("lastActiveTime") LocalDateTime lastActiveTime);
} }

View File

@ -13,7 +13,8 @@ import com.red.circle.other.inner.model.cmd.user.UserBaseInfoPageQryCmd;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -34,7 +35,38 @@ public interface BaseInfoService extends BaseService<BaseInfo> {
/** /**
* 获取用户信息. * 获取用户信息.
*/ */
List<BaseInfo> listUser(SysOriginUserQryCmd qryCmd); List<BaseInfo> listUser(SysOriginUserQryCmd qryCmd);
/**
* 按国家ID游标读取用户避免批量同步时一次加载全部用户.
*/
List<BaseInfo> listCountryUsersAfterId(Long countryId, Long lastId, Integer limit);
/**
* 查询国家下涉及的应用来源.
*/
Set<String> listOriginSysByCountryId(Long countryId);
/**
* 查询国家下当前存在的用户国家码.
*/
Set<String> listCountryCodesByCountryId(Long countryId);
/**
* 国家编码冗余字段是否存在未同步数据.
*/
boolean existsCountryCodeMismatch(Long countryId, String countryCode);
/**
* 国家名称冗余字段是否存在未同步数据.
*/
boolean existsCountryNameMismatch(Long countryId, String countryName);
/**
* 按国家ID约束批量同步用户国家资料.
*/
int updateCountryByIds(Long countryId, Collection<Long> userIds, String countryCode,
String countryName);
/** /**
* 归属系统. * 归属系统.

View File

@ -2,8 +2,9 @@ package com.red.circle.other.infra.database.rds.service.user.user;
import com.red.circle.framework.mybatis.service.BaseService; import com.red.circle.framework.mybatis.service.BaseService;
import com.red.circle.other.infra.database.rds.entity.user.user.UserExpand; import com.red.circle.other.infra.database.rds.entity.user.user.UserExpand;
import com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO; import com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -17,14 +18,20 @@ import java.util.Set;
* @author pengliang * @author pengliang
* @since 2019-10-30 * @since 2019-10-30
*/ */
public interface ExpandService extends BaseService<UserExpand> { public interface ExpandService extends BaseService<UserExpand> {
/**
* 国家码重命名时同步仍指向旧码的注册国家快照.
*/
int updateRegisterCountryCodeByCountry(Long countryId, Collection<String> sourceCountryCodes,
String newCountryCode);
/** /**
* 不存在创建. * 不存在创建.
* *
* @param userExpand ignore * @param userExpand ignore
*/ */
void notExistsCreate(UserExpand userExpand); void notExistsCreate(UserExpand userExpand);
/** /**
* 初始化扩展信息. * 初始化扩展信息.

View File

@ -55,7 +55,7 @@ public class BaseInfoServiceImpl extends BaseServiceImpl<BaseInfoDAO, BaseInfo>
} }
@Override @Override
public List<BaseInfo> listUser(SysOriginUserQryCmd qryCmd) { public List<BaseInfo> listUser(SysOriginUserQryCmd qryCmd) {
//DAY,MONTH,YEAR //DAY,MONTH,YEAR
return query() return query()
.eq(StringUtils.isNotBlank(qryCmd.getCountryCode()), BaseInfo::getCountryCode, qryCmd.getCountryCode()) .eq(StringUtils.isNotBlank(qryCmd.getCountryCode()), BaseInfo::getCountryCode, qryCmd.getCountryCode())
@ -63,8 +63,42 @@ public class BaseInfoServiceImpl extends BaseServiceImpl<BaseInfoDAO, BaseInfo>
TimestampUtils.convert(getStartDate(qryCmd.getDate(), qryCmd.getDateType()))) TimestampUtils.convert(getStartDate(qryCmd.getDate(), qryCmd.getDateType())))
.le(StringUtils.isNotBlank(qryCmd.getDate()), BaseInfo::getCreateTime, .le(StringUtils.isNotBlank(qryCmd.getDate()), BaseInfo::getCreateTime,
TimestampUtils.convert(getEndDate(qryCmd.getDate(), qryCmd.getDateType()))) TimestampUtils.convert(getEndDate(qryCmd.getDate(), qryCmd.getDateType())))
.list(); .list();
} }
@Override
public List<BaseInfo> listCountryUsersAfterId(Long countryId, Long lastId, Integer limit) {
return baseInfoDAO.listCountryUsersAfterId(countryId, lastId, limit);
}
@Override
public Set<String> listOriginSysByCountryId(Long countryId) {
return new HashSet<>(baseInfoDAO.listDistinctOriginSysByCountryId(countryId));
}
@Override
public Set<String> listCountryCodesByCountryId(Long countryId) {
return new HashSet<>(baseInfoDAO.listDistinctCountryCodesByCountryId(countryId));
}
@Override
public boolean existsCountryCodeMismatch(Long countryId, String countryCode) {
return Boolean.TRUE.equals(baseInfoDAO.existsCountryCodeMismatch(countryId, countryCode));
}
@Override
public boolean existsCountryNameMismatch(Long countryId, String countryName) {
return Boolean.TRUE.equals(baseInfoDAO.existsCountryNameMismatch(countryId, countryName));
}
@Override
public int updateCountryByIds(Long countryId, Collection<Long> userIds, String countryCode,
String countryName) {
if (CollectionUtils.isEmpty(userIds)) {
return 0;
}
return baseInfoDAO.updateCountryByIds(countryId, userIds, countryCode, countryName);
}
/** /**
* 获取开始时间 * 获取开始时间

View File

@ -7,8 +7,9 @@ import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.other.infra.database.rds.dao.user.user.ExpandDAO; import com.red.circle.other.infra.database.rds.dao.user.user.ExpandDAO;
import com.red.circle.other.infra.database.rds.entity.user.user.UserExpand; import com.red.circle.other.infra.database.rds.entity.user.user.UserExpand;
import com.red.circle.other.infra.database.rds.service.user.user.ExpandService; import com.red.circle.other.infra.database.rds.service.user.user.ExpandService;
import com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO; import com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -30,9 +31,22 @@ import org.springframework.stereotype.Service;
*/ */
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
public class ExpandServiceImpl extends BaseServiceImpl<ExpandDAO, UserExpand> implements ExpandService { public class ExpandServiceImpl extends BaseServiceImpl<ExpandDAO, UserExpand> implements ExpandService {
private Boolean exists(Long userId) { private final ExpandDAO expandDAO;
@Override
public int updateRegisterCountryCodeByCountry(Long countryId,
Collection<String> sourceCountryCodes,
String newCountryCode) {
if (countryId == null || CollectionUtils.isEmpty(sourceCountryCodes)) {
return 0;
}
return expandDAO.updateRegisterCountryCodeByCountry(countryId, sourceCountryCodes,
newCountryCode);
}
private Boolean exists(Long userId) {
return Optional.ofNullable( return Optional.ofNullable(
query().select(UserExpand::getUserId).eq(UserExpand::getUserId, userId).last(PageConstant.LIMIT_ONE) query().select(UserExpand::getUserId).eq(UserExpand::getUserId, userId).last(PageConstant.LIMIT_ONE)
.getOne() .getOne()

View File

@ -428,12 +428,15 @@ public class UserProfileGatewayImpl implements UserProfileGateway {
} }
@Override @Override
public void removeCacheAll(Collection<Long> ids) { public void removeCacheAll(Collection<Long> ids) {
if (CollectionUtils.isNotEmpty(ids)) { if (CollectionUtils.isNotEmpty(ids)) {
userRunProfileService.remove(ids); userRunProfileService.remove(ids);
userCacheService.remove(ids); userCacheService.remove(ids);
} // 批量失效必须与单用户路径一致否则国家或区域修改后仍会读到旧运行缓存.
} userRunProfileCacheService.remove(ids);
userRegionCacheService.remove(ids);
}
}
@Override @Override
public void removeCache(Long id) { public void removeCache(Long id) {

View File

@ -215,14 +215,14 @@ public class UserRunProfileTransportGatewayImpl implements UserRunProfileTranspo
} }
@Override @Override
public UserProfile updateUserRunProfile(Long id, String accountStatus) { public UserProfile updateUserRunProfile(Long id, String accountStatus) {
UserRunProfile userRunProfiles = userRunProfileService.getById(id); UserRunProfile userRunProfiles = userRunProfileService.getById(id);
if (Objects.nonNull(userRunProfiles)) { if (Objects.nonNull(userRunProfiles)) {
userRunProfiles.setAccountStatus(accountStatus); userRunProfileService.updateAccountStatus(id, accountStatus);
userRunProfiles.setUpdateTime(TimestampUtils.now()); userRunProfiles.setAccountStatus(accountStatus);
userRunProfileService.updateSelectiveById(userRunProfiles); userRunProfiles.setUpdateTime(TimestampUtils.now());
return userProfileInfraConvertor.toUserProfile(userRunProfiles); return userProfileInfraConvertor.toUserProfile(userRunProfiles);
} }
return null; return null;
} }

View File

@ -1,8 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.red.circle.other.infra.database.rds.dao.user.user.BaseInfoDAO"> <mapper namespace="com.red.circle.other.infra.database.rds.dao.user.user.BaseInfoDAO">
<select id="maxUserAccount" resultType="java.lang.Long"> <select id="listCountryUsersAfterId"
resultType="com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo">
SELECT id,
country_code AS countryCode,
country_name AS countryName,
origin_sys AS originSys
FROM user_base_info
WHERE country_id = #{countryId}
AND id &gt; #{lastId}
ORDER BY id ASC
LIMIT #{limit}
</select>
<select id="listDistinctOriginSysByCountryId" resultType="java.lang.String">
SELECT DISTINCT origin_sys
FROM user_base_info
WHERE country_id = #{countryId}
AND origin_sys IS NOT NULL
AND origin_sys != ''
</select>
<select id="listDistinctCountryCodesByCountryId" resultType="java.lang.String">
SELECT DISTINCT country_code
FROM user_base_info
WHERE country_id = #{countryId}
AND country_code IS NOT NULL
AND country_code != ''
</select>
<select id="existsCountryCodeMismatch" resultType="java.lang.Boolean">
SELECT EXISTS(
SELECT 1
FROM user_base_info
WHERE country_id = #{countryId}
AND (country_code IS NULL OR country_code != #{countryCode})
LIMIT 1
)
</select>
<select id="existsCountryNameMismatch" resultType="java.lang.Boolean">
SELECT EXISTS(
SELECT 1
FROM user_base_info
WHERE country_id = #{countryId}
AND (country_name IS NULL OR country_name != #{countryName})
LIMIT 1
)
</select>
<update id="updateCountryByIds">
UPDATE user_base_info
SET country_code = #{countryCode},
country_name = #{countryName},
update_time = CURRENT_TIMESTAMP
WHERE country_id = #{countryId}
AND id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
</update>
<select id="maxUserAccount" resultType="java.lang.Long">
SELECT MAX(account + 0) SELECT MAX(account + 0)
FROM user_base_info FROM user_base_info
</select> </select>

View File

@ -1,9 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.red.circle.other.infra.database.rds.dao.user.user.ExpandDAO"> <mapper namespace="com.red.circle.other.infra.database.rds.dao.user.user.ExpandDAO">
<select id="findLatestActiveUserCountryCode" <update id="updateRegisterCountryCodeByCountry">
UPDATE user_expand ue
INNER JOIN user_base_info ubi ON ubi.id = ue.user_id
SET ue.register_country_code = #{newCountryCode},
ue.update_time = CURRENT_TIMESTAMP
WHERE ubi.country_id = #{countryId}
AND ue.register_country_code IN
<foreach collection="sourceCountryCodes" item="sourceCountryCode" open="(" separator="," close=")">
#{sourceCountryCode}
</foreach>
</update>
<select id="findLatestActiveUserCountryCode"
resultType="com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO"> resultType="com.red.circle.other.inner.model.dto.user.ActiveUserCountryCodeDTO">
SELECT alpha_three AS name, SELECT alpha_three AS name,
COUNT(1) AS `value` COUNT(1) AS `value`

View File

@ -0,0 +1,32 @@
package com.red.circle.other.app.inner.service.sys;
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.infra.database.rds.service.user.user.ExpandService;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 国家主记录与注册国家快照的同库事务更新服务.
*/
@Service
@RequiredArgsConstructor
public class CountryCodeRdsUpdateService {
private final ExpandService expandService;
private final SysCountryCodeService sysCountryCodeService;
/**
* 先更新国家主记录以触发唯一键校验再在同一事务内重命名旧注册国家码.
* 任一步失败都会回滚避免国家主记录已变更但注册快照仍残留旧码.
*/
@Transactional(rollbackFor = Exception.class)
public int update(SysCountryCode countryUpdate, Set<String> sourceCountryCodes,
String newCountryCode) {
sysCountryCodeService.updateCountryCode(countryUpdate);
return expandService.updateRegisterCountryCodeByCountry(countryUpdate.getId(),
sourceCountryCodes, newCountryCode);
}
}

View File

@ -0,0 +1,474 @@
package com.red.circle.other.app.inner.service.sys;
import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
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.infra.database.cache.service.user.UserCacheService;
import com.red.circle.other.infra.database.cache.service.user.RegionRoomCacheService;
import com.red.circle.other.infra.database.cache.service.user.UserRegionCacheService;
import com.red.circle.other.infra.database.cache.service.user.UserRunProfileCacheService;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfile;
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.entity.user.region.SysRegionConfig;
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.mongo.service.activity.AgentActivityCountService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamBillCycleService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberTargetService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileService;
import com.red.circle.other.infra.database.mongo.service.user.active.UserDailyActiveService;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserRunProfileService;
import com.red.circle.other.infra.database.mongo.service.user.region.SysRegionConfigService;
import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo;
import com.red.circle.other.infra.database.rds.service.user.user.BaseInfoService;
import com.red.circle.other.infra.common.user.UserCountryWriteLockService;
import com.red.circle.other.inner.model.dto.agency.agency.TeamCountry;
import com.red.circle.other.inner.model.cache.EmptyRoomCacheKeys;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Collection;
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.stereotype.Service;
/**
* 国家资料冗余字段同步服务.
*
* <p>用户和团队均以不可变 countryId 为归属依据国家码只作为需要同步的属性
* 避免新旧国家曾共用同一编码时误迁数据</p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CountryDataSyncService {
private static final int USER_BATCH_SIZE = 300;
private static final int TEAM_BATCH_SIZE = 200;
private final BaseInfoService baseInfoService;
private final RedisService redisService;
private final RegionRoomCacheService regionRoomCacheService;
private final UserCountryWriteLockService userCountryWriteLockService;
private final AgentActivityCountService agentActivityCountService;
private final TeamBillCycleService teamBillCycleService;
private final TeamMemberService teamMemberService;
private final TeamMemberTargetService teamMemberTargetService;
private final TeamProfileService teamProfileService;
private final UserCacheService userCacheService;
private final UserRegionCacheService userRegionCacheService;
private final UserRunProfileCacheService userRunProfileCacheService;
private final UserRunProfileService userRunProfileService;
private final RoomProfileManagerService roomProfileManagerService;
private final ActiveVoiceRoomService activeVoiceRoomService;
private final UserDailyActiveService userDailyActiveService;
private final SysRegionConfigService sysRegionConfigService;
/**
* 在写国家主记录前生成同步计划并校验每个应用来源的目标区域唯一性.
*/
public CountrySyncPlan prepare(Long countryId, String oldCountryCode, String targetCountryCode,
String targetCountryName, boolean countryCodeChanged, boolean countryNameChanged) {
boolean userCodeMismatch = baseInfoService.existsCountryCodeMismatch(countryId,
targetCountryCode);
boolean userNameMismatch = baseInfoService.existsCountryNameMismatch(countryId,
targetCountryName);
boolean teamCodeMismatch = teamProfileService.existsCountryCodeMismatch(countryId,
targetCountryCode);
boolean teamNameMismatch = teamProfileService.existsCountryNameMismatch(countryId,
targetCountryName);
// 后台表单始终会提交 alphaTwo因此以真实变更或编码残留判定是否重算区域.
boolean syncRegion = countryCodeChanged || userCodeMismatch || teamCodeMismatch;
Set<String> registerSourceCountryCodes = new HashSet<>();
if (syncRegion) {
registerSourceCountryCodes.addAll(
baseInfoService.listCountryCodesByCountryId(countryId).stream()
.filter(StringUtils::isNotBlank)
.filter(code -> !sameCountryCode(code, targetCountryCode))
.collect(Collectors.toSet()));
if (countryCodeChanged && StringUtils.isNotBlank(oldCountryCode)
&& !sameCountryCode(oldCountryCode, targetCountryCode)) {
registerSourceCountryCodes.add(oldCountryCode);
}
}
Map<String, TargetRegion> targetRegions;
boolean teamRegionMismatch;
if (syncRegion) {
Set<String> origins = new HashSet<>(baseInfoService.listOriginSysByCountryId(countryId));
origins.addAll(teamProfileService.listOriginSysByCountryId(countryId));
origins.removeIf(StringUtils::isBlank);
targetRegions = resolveTargetRegions(countryId, targetCountryCode, origins);
teamRegionMismatch = targetRegions.entrySet().stream()
.anyMatch(entry -> teamProfileService.existsCountryRegionMismatch(countryId,
entry.getKey(), entry.getValue().regionId()));
} else {
targetRegions = Map.of();
teamRegionMismatch = false;
}
boolean syncRequired = countryCodeChanged || countryNameChanged || userCodeMismatch
|| userNameMismatch || teamCodeMismatch || teamNameMismatch || teamRegionMismatch;
return new CountrySyncPlan(syncRequired, syncRegion, targetRegions,
registerSourceCountryCodes);
}
/**
* 执行用户与团队同步所有步骤均可按 countryId 重复执行.
*/
public CountrySyncResult synchronize(SysCountryCode oldCountry, SysCountryCode targetCountry,
Long updateUser, CountrySyncPlan plan) {
if (!plan.syncRequired()) {
return new CountrySyncResult(0, 0, 0);
}
// 先改用户再扫团队建团流程会从用户取国家快照可避免批次末尾新团队落入旧编码.
int userCount = synchronizeUsers(targetCountry, plan);
TeamSyncResult teamResult = synchronizeTeams(targetCountry, updateUser, plan);
validateSynchronized(targetCountry, plan);
log.info("国家资料级联同步完成 countryId={}, code={} -> {}, users={}, teams={}, movedTeams={}",
targetCountry.getId(), oldCountry.getAlphaTwo(), targetCountry.getAlphaTwo(), userCount,
teamResult.updatedTeams(), teamResult.movedTeams());
return new CountrySyncResult(userCount, teamResult.updatedTeams(), teamResult.movedTeams());
}
private void validateSynchronized(SysCountryCode targetCountry, CountrySyncPlan plan) {
boolean mismatch = baseInfoService.existsCountryCodeMismatch(targetCountry.getId(),
targetCountry.getAlphaTwo())
|| baseInfoService.existsCountryNameMismatch(targetCountry.getId(),
targetCountry.getCountryName())
|| teamProfileService.existsCountryCodeMismatch(targetCountry.getId(),
targetCountry.getAlphaTwo())
|| teamProfileService.existsCountryNameMismatch(targetCountry.getId(),
targetCountry.getCountryName());
if (plan.syncRegion()) {
mismatch = mismatch || plan.targetRegionByOrigin().entrySet().stream()
.anyMatch(entry -> teamProfileService.existsCountryRegionMismatch(targetCountry.getId(),
entry.getKey(), entry.getValue().regionId()));
}
// 结束前二次校验游标后方新数据不一致则明确失败同值保存可幂等重试.
ResponseAssert.isFalse(CommonErrorCode.OPERATION_CONFLICT, mismatch);
}
private int synchronizeUsers(SysCountryCode targetCountry, CountrySyncPlan plan) {
long lastId = 0L;
int updated = 0;
while (true) {
List<BaseInfo> users = baseInfoService.listCountryUsersAfterId(targetCountry.getId(), lastId,
USER_BATCH_SIZE);
if (CollectionUtils.isEmpty(users)) {
break;
}
List<Long> userIds = users.stream().map(BaseInfo::getId).filter(Objects::nonNull).toList();
if (CollectionUtils.isEmpty(userIds)) {
break;
}
Integer batchUpdated = userCountryWriteLockService.execute(userIds, () -> {
// 加锁后重查稳定countryId剔除刚被后台切到其他国家的用户.
List<BaseInfo> confirmedUsers = baseInfoService.listByUserId(userIds).stream()
.filter(user -> Objects.equals(user.getCountryId(), targetCountry.getId()))
.toList();
List<Long> confirmedUserIds = confirmedUsers.stream().map(BaseInfo::getId).toList();
if (CollectionUtils.isEmpty(confirmedUserIds)) {
return 0;
}
List<ActiveVoiceRoom> activeRooms = activeVoiceRoomService.listByUserIds(
confirmedUserIds);
List<RoomProfile> roomProfiles = roomProfileManagerService.listProfileByUserIds(
new HashSet<>(confirmedUserIds));
// 先同步跨库投影user_base_info 最后落盘作为本批次完成标记失败后同值保存可重试.
userRunProfileService.updateCountry(confirmedUserIds, targetCountry.getId(),
targetCountry.getAlphaTwo(), targetCountry.getCountryName());
roomProfileManagerService.updateUserCountry(confirmedUserIds, targetCountry.getAlphaTwo(),
targetCountry.getCountryName());
activeVoiceRoomService.updateUserCountry(confirmedUserIds, targetCountry.getAlphaTwo(),
targetCountry.getCountryName());
if (plan.syncRegion()) {
updateActiveRoomRegions(confirmedUsers, plan);
}
userDailyActiveService.updateTodayCountryCode(confirmedUserIds,
targetCountry.getAlphaTwo());
teamMemberTargetService.updateCurrentCountryCodeByUserIds(confirmedUserIds,
targetCountry.getAlphaTwo());
clearUserCaches(confirmedUserIds);
// 发现页缓存与空房快照都包含国家/区域冗余字段主表落盘前先清理以保留重试标记.
clearRoomCaches(activeRooms, roomProfiles, plan.targetRegionByOrigin());
int affected = baseInfoService.updateCountryByIds(targetCountry.getId(), confirmedUserIds,
targetCountry.getAlphaTwo(), targetCountry.getCountryName());
clearUserCachesBestEffort(confirmedUserIds);
clearRoomCachesBestEffort(activeRooms, roomProfiles, plan.targetRegionByOrigin());
return affected;
});
updated += batchUpdated;
lastId = userIds.get(userIds.size() - 1);
}
return updated;
}
private TeamSyncResult synchronizeTeams(SysCountryCode targetCountry, Long updateUser,
CountrySyncPlan plan) {
int updatedTeams = 0;
int movedTeams = 0;
TeamCountry targetTeamCountry = new TeamCountry()
.setCountryId(targetCountry.getId())
.setCountryCode(targetCountry.getAlphaTwo())
.setCountryName(targetCountry.getCountryName());
long lastTeamId = 0L;
while (true) {
List<TeamProfile> teams = teamProfileService.listCountryTeamsAfterId(targetCountry.getId(),
lastTeamId, TEAM_BATCH_SIZE);
if (CollectionUtils.isEmpty(teams)) {
break;
}
for (TeamProfile currentTeam : teams) {
lastTeamId = currentTeam.getId();
List<Long> ownerLockIds = currentTeam.getOwnUserId() == null
? List.of() : List.of(currentTeam.getOwnUserId());
TeamItemSyncResult itemResult = userCountryWriteLockService.execute(ownerLockIds,
() -> synchronizeTeam(targetCountry, updateUser, plan, targetTeamCountry,
currentTeam.getId()));
if (itemResult.updated()) {
updatedTeams++;
if (itemResult.moved()) {
movedTeams++;
}
}
}
}
return new TeamSyncResult(updatedTeams, movedTeams);
}
private TeamItemSyncResult synchronizeTeam(SysCountryCode targetCountry, Long updateUser,
CountrySyncPlan plan, TeamCountry targetTeamCountry, Long teamId) {
// 取得团长国家锁后重读避免扫描到正在初始化的半成品团队.
TeamProfile currentTeam = teamProfileService.getById(teamId);
if (currentTeam == null || currentTeam.getCountry() == null
|| !Objects.equals(currentTeam.getCountry().getCountryId(), targetCountry.getId())) {
return new TeamItemSyncResult(false, false);
}
TargetRegion targetRegion = plan.syncRegion()
? plan.targetRegionByOrigin().get(currentTeam.getSysOrigin())
: new TargetRegion(currentTeam.getRegion(), null);
ResponseAssert.isTrue(CommonErrorCode.CONFIGURATION_ERROR, targetRegion != null
&& StringUtils.isNotBlank(targetRegion.regionId()));
boolean regionChanged = !Objects.equals(currentTeam.getRegion(), targetRegion.regionId());
Set<Long> memberIds = regionChanged
? teamMemberService.getMemberIdsByTeamId(currentTeam.getId()) : Set.of();
if (regionChanged) {
synchronizeTeamRegionProjections(currentTeam, targetRegion, memberIds, updateUser);
}
// team_profile 最后以当前区域做CAS写入避免覆盖并发发生的手工团队迁区.
boolean teamUpdated = teamProfileService.updateCountryAndRegion(currentTeam.getId(),
targetCountry.getId(), currentTeam.getRegion(), targetRegion.regionId(),
targetTeamCountry, updateUser);
if (!teamUpdated && regionChanged) {
// CAS失败说明团队被并发修改立即按最新团队区域恢复先行投影避免覆盖人工迁区结果.
restoreTeamRegionProjections(currentTeam.getId(), memberIds, updateUser);
}
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT, teamUpdated);
if (regionChanged) {
clearUserRegionCachesBestEffort(memberIds);
}
return new TeamItemSyncResult(true, regionChanged);
}
private void synchronizeTeamRegionProjections(TeamProfile team, TargetRegion targetRegion,
Set<Long> memberIds, Long updateUser) {
List<ActiveVoiceRoom> activeRooms = CollectionUtils.isEmpty(memberIds)
? CollectionUtils.newArrayList() : activeVoiceRoomService.listByUserIds(memberIds);
List<RoomProfile> roomProfiles = CollectionUtils.isEmpty(memberIds)
? CollectionUtils.newArrayList()
: roomProfileManagerService.listProfileByUserIds(memberIds);
if (CollectionUtils.isNotEmpty(memberIds)) {
// 活跃房间保存的是区域编码定向置顶房间由服务层保留其固定区域.
activeVoiceRoomService.updateUserDefaultRegion(memberIds, targetRegion.regionCode());
userRegionCacheService.remove(memberIds);
}
// 仅迁移当前有效数据保留历史账单累计流水目标和活动积分.
teamBillCycleService.updateUnpaidTeamBillRegion(team.getId(), targetRegion.regionId(),
updateUser);
teamMemberTargetService.updateCurrentRegionByTeamId(team.getId(), targetRegion.regionId());
agentActivityCountService.updateCurrentRegion(team.getId(), targetRegion.regionId());
clearRoomCaches(activeRooms, roomProfiles, Map.of(team.getSysOrigin(), targetRegion));
}
private void restoreTeamRegionProjections(Long teamId, Set<Long> memberIds, Long updateUser) {
TeamProfile latestTeam = teamProfileService.getById(teamId);
if (latestTeam == null || StringUtils.isBlank(latestTeam.getRegion())) {
return;
}
SysRegionConfig latestRegion = sysRegionConfigService.getById(latestTeam.getRegion());
if (latestRegion == null || StringUtils.isBlank(latestRegion.getRegionCode())) {
return;
}
synchronizeTeamRegionProjections(latestTeam,
new TargetRegion(latestRegion.getId(), latestRegion.getRegionCode()), memberIds, updateUser);
}
private void clearRoomCaches(List<ActiveVoiceRoom> activeRooms, List<RoomProfile> roomProfiles,
Map<String, TargetRegion> targetRegions) {
Set<String> discoveryKeys = new HashSet<>();
for (ActiveVoiceRoom room : activeRooms) {
if (StringUtils.isNotBlank(room.getSysOrigin()) && StringUtils.isNotBlank(room.getRegion())) {
discoveryKeys.add(room.getSysOrigin() + room.getRegion());
discoveryKeys.add(room.getSysOrigin() + room.getRegion() + ":ALL_REGION");
}
}
targetRegions.forEach((origin, region) -> {
if (StringUtils.isNotBlank(origin) && region != null
&& StringUtils.isNotBlank(region.regionCode())) {
discoveryKeys.add(origin + region.regionCode());
discoveryKeys.add(origin + region.regionCode() + ":ALL_REGION");
}
});
discoveryKeys.forEach(regionRoomCacheService::remove);
List<String> emptyRoomPayloadKeys = roomProfiles.stream().map(RoomProfile::getId)
.filter(Objects::nonNull).map(EmptyRoomCacheKeys::payload).toList();
if (CollectionUtils.isNotEmpty(emptyRoomPayloadKeys)) {
// payload 删除后小型区域ZSet中的旧成员会被 reader 有界清理无需全库扫描.
redisService.delete(emptyRoomPayloadKeys);
}
}
private void clearRoomCachesBestEffort(List<ActiveVoiceRoom> activeRooms,
List<RoomProfile> roomProfiles, Map<String, TargetRegion> targetRegions) {
try {
clearRoomCaches(activeRooms, roomProfiles, targetRegions);
} catch (RuntimeException ex) {
log.warn("国家资料同步后二次房间缓存失效失败", ex);
}
}
private Map<String, TargetRegion> resolveTargetRegions(Long countryId, String countryCode,
Set<String> origins) {
Map<String, TargetRegion> result = new HashMap<>();
for (String origin : origins) {
List<SysRegionConfig> matched = sysRegionConfigService.listAvailable(origin).stream()
.filter(region -> containsExactCountryCode(region.getCountryCodes(), countryCode))
.toList();
if (matched.size() != 1) {
log.error("国家码目标区域配置不唯一 countryId={}, countryCode={}, sysOrigin={}, matches={}",
countryId, countryCode, origin,
matched.stream().map(SysRegionConfig::getId).collect(Collectors.toList()));
}
ResponseAssert.isTrue(CommonErrorCode.CONFIGURATION_ERROR, matched.size() == 1
&& StringUtils.isNotBlank(matched.get(0).getRegionCode()));
result.put(origin, new TargetRegion(matched.get(0).getId(),
matched.get(0).getRegionCode()));
}
return result;
}
private boolean containsExactCountryCode(String csvCodes, String countryCode) {
if (StringUtils.isBlank(csvCodes) || StringUtils.isBlank(countryCode)) {
return false;
}
String normalized = CountryCodeAliasUtils.normalize(countryCode);
for (String code : csvCodes.split(",")) {
if (Objects.equals(normalized, CountryCodeAliasUtils.normalize(code))) {
return true;
}
}
return false;
}
private boolean sameCountryCode(String left, String right) {
if (StringUtils.isBlank(left) || StringUtils.isBlank(right)) {
return Objects.equals(left, right);
}
return Objects.equals(CountryCodeAliasUtils.normalize(left),
CountryCodeAliasUtils.normalize(right));
}
private void updateActiveRoomRegions(List<BaseInfo> users, CountrySyncPlan plan) {
Set<Long> userIds = users.stream().map(BaseInfo::getId).filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, TeamMember> memberByUser = teamMemberService.mapByMemberIds(userIds);
Map<Long, TeamProfile> teamById = teamProfileService.mapProfileByIds(memberByUser.values()
.stream().map(TeamMember::getTeamId).filter(Objects::nonNull).collect(Collectors.toSet()));
Set<String> teamRegionIds = teamById.values().stream().map(TeamProfile::getRegion)
.filter(StringUtils::isNotBlank).collect(Collectors.toSet());
Map<String, SysRegionConfig> teamRegions = sysRegionConfigService.mapAvailable(teamRegionIds);
Map<String, List<Long>> usersByRegionCode = new HashMap<>();
for (BaseInfo user : users) {
TeamMember member = memberByUser.get(user.getId());
TeamProfile team = member == null ? null : teamById.get(member.getTeamId());
SysRegionConfig teamRegion = team == null ? null : teamRegions.get(team.getRegion());
String regionCode = teamRegion == null ? null : teamRegion.getRegionCode();
if (StringUtils.isBlank(regionCode)) {
TargetRegion countryRegion = plan.targetRegionByOrigin().get(user.getOriginSys());
regionCode = countryRegion == null ? null : countryRegion.regionCode();
}
ResponseAssert.isTrue(CommonErrorCode.CONFIGURATION_ERROR,
StringUtils.isNotBlank(regionCode));
usersByRegionCode.computeIfAbsent(regionCode, ignored -> CollectionUtils.newArrayList())
.add(user.getId());
}
usersByRegionCode.forEach(
(regionCode, regionUserIds) -> activeVoiceRoomService.updateUserDefaultRegion(
regionUserIds, regionCode));
}
private void clearUserCaches(List<Long> userIds) {
userRunProfileCacheService.remove(userIds);
userRegionCacheService.remove(userIds);
userCacheService.remove(userIds);
}
private void clearUserCachesBestEffort(List<Long> userIds) {
try {
// 第二次失效封住第一次删缓存到主表落盘之间极短窗口内被回填的旧值.
clearUserCaches(userIds);
} catch (RuntimeException ex) {
log.warn("国家资料同步后二次缓存失效失败 userCount={}", userIds.size(), ex);
}
}
private void clearUserRegionCachesBestEffort(Collection<Long> userIds) {
try {
userRegionCacheService.remove(userIds);
} catch (RuntimeException ex) {
log.warn("团队区域同步后二次区域缓存失效失败 userCount={}", userIds.size(), ex);
}
}
public record CountrySyncPlan(boolean syncRequired, boolean syncRegion,
Map<String, TargetRegion> targetRegionByOrigin,
Set<String> registerSourceCountryCodes) {
}
public record TargetRegion(String regionId, String regionCode) {
}
public record CountrySyncResult(int updatedUsers, int updatedTeams, int movedTeams) {
}
private record TeamSyncResult(int updatedTeams, int movedTeams) {
}
private record TeamItemSyncResult(boolean updated, boolean moved) {
}
}

View File

@ -1,29 +1,44 @@
package com.red.circle.other.app.inner.service.sys.impl; package com.red.circle.other.app.inner.service.sys.impl;
import com.red.circle.framework.dto.PageResult; import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.other.app.inner.service.sys.SysCountryCodeClientService; import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.other.infra.convertor.sys.CountryCodeInnerConvertor; import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService; import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.other.inner.model.cmd.sys.SysCountryCodeCmd; import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.model.cmd.sys.SysCountryCodePageQryCmd; import com.red.circle.other.app.inner.service.sys.CountryCodeRdsUpdateService;
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO; import com.red.circle.other.app.inner.service.sys.CountryDataSyncService;
import java.util.List; import com.red.circle.other.app.inner.service.sys.SysCountryCodeClientService;
import java.util.Map; import com.red.circle.other.infra.common.sys.CountryDataSyncLockService;
import java.util.Set; import com.red.circle.other.infra.convertor.sys.CountryCodeInnerConvertor;
import lombok.RequiredArgsConstructor; import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import org.springframework.stereotype.Service; import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
import com.red.circle.other.inner.model.cmd.sys.SysCountryCodeCmd;
import com.red.circle.other.inner.model.cmd.sys.SysCountryCodePageQryCmd;
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
import com.red.circle.tool.core.text.StringUtils;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/** /**
* 系统国家服务 实现. * 系统国家服务 实现.
* *
* @author pengliang on 2023/5/24 * @author pengliang on 2023/5/24
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SysCountryCodeClientServiceImpl implements SysCountryCodeClientService { @Slf4j
public class SysCountryCodeClientServiceImpl implements SysCountryCodeClientService {
private final SysCountryCodeService sysCountryCodeService;
private final CountryCodeInnerConvertor countryCodeInnerConvertor; private final SysCountryCodeService sysCountryCodeService;
private final CountryCodeInnerConvertor countryCodeInnerConvertor;
private final CountryDataSyncService countryDataSyncService;
private final CountryDataSyncLockService countryDataSyncLockService;
private final CountryCodeRdsUpdateService countryCodeRdsUpdateService;
@Override @Override
public SysCountryCodeDTO getById(Long id) { public SysCountryCodeDTO getById(Long id) {
@ -71,9 +86,63 @@ public class SysCountryCodeClientServiceImpl implements SysCountryCodeClientServ
.convert(countryCodeInnerConvertor::toSysCountryCodeDTO); .convert(countryCodeInnerConvertor::toSysCountryCodeDTO);
} }
@Override @Override
public void updateCountryCode(SysCountryCodeCmd dto) { public void updateCountryCode(SysCountryCodeCmd dto) {
sysCountryCodeService.updateCountryCode(countryCodeInnerConvertor.toSysCountryCode(dto)); SysCountryCode countryUpdate = countryCodeInnerConvertor.toSysCountryCode(dto);
if (dto.getId() == null) {
sysCountryCodeService.updateCountryCode(countryUpdate);
return;
}
// 开放置顶等轻量操作也复用此接口只有提交了国家编码或名称才检查级联同步.
boolean canonicalFieldsSubmitted = dto.getAlphaTwo() != null || dto.getCountryName() != null;
if (!canonicalFieldsSubmitted) {
sysCountryCodeService.updateCountryCode(countryUpdate);
return;
}
countryDataSyncLockService.execute(dto.getId(), () -> {
SysCountryCode oldCountry = sysCountryCodeService.getById(dto.getId());
ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, oldCountry);
String targetCode = dto.getAlphaTwo() == null
? oldCountry.getAlphaTwo()
: CountryCodeAliasUtils.normalize(dto.getAlphaTwo());
String targetName = dto.getCountryName() == null
? oldCountry.getCountryName()
: dto.getCountryName().trim();
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
StringUtils.isNotBlank(targetCode) && StringUtils.isNotBlank(targetName));
if (dto.getAlphaTwo() != null) {
countryUpdate.setAlphaTwo(targetCode);
}
if (dto.getCountryName() != null) {
countryUpdate.setCountryName(targetName);
}
boolean countryCodeChanged = !Objects.equals(
CountryCodeAliasUtils.normalize(oldCountry.getAlphaTwo()), targetCode);
boolean countryNameChanged = !Objects.equals(oldCountry.getCountryName(), targetName);
CountryDataSyncService.CountrySyncPlan syncPlan = countryDataSyncService.prepare(
oldCountry.getId(), oldCountry.getAlphaTwo(), targetCode, targetName,
countryCodeChanged, countryNameChanged);
// 国家主记录与注册国家快照在一个RDS事务内完成跨库用户/团队投影随后按countryId幂等同步.
int registerSnapshots = countryCodeRdsUpdateService.update(countryUpdate,
syncPlan.registerSourceCountryCodes(), targetCode);
if (syncPlan.syncRequired()) {
SysCountryCode targetCountry = new SysCountryCode()
.setId(oldCountry.getId())
.setAlphaTwo(targetCode)
.setCountryName(targetName);
CountryDataSyncService.CountrySyncResult result = countryDataSyncService.synchronize(
oldCountry, targetCountry, dto.getUpdateUser(), syncPlan);
log.info("国家资料更新同步完成 countryId={}, registerSnapshots={}, result={}",
oldCountry.getId(), registerSnapshots, result);
}
return null;
});
} }
@Override @Override

View File

@ -17,10 +17,12 @@ import com.red.circle.mq.rocket.business.service.MessageSenderService;
import com.red.circle.mq.rocket.business.streams.TeamSalaryCountSink; import com.red.circle.mq.rocket.business.streams.TeamSalaryCountSink;
import com.red.circle.other.app.inner.service.team.TeamManualSalaryPaymentClientService; import com.red.circle.other.app.inner.service.team.TeamManualSalaryPaymentClientService;
import com.red.circle.other.app.inner.service.team.TeamProfileClientService; import com.red.circle.other.app.inner.service.team.TeamProfileClientService;
import com.red.circle.other.domain.gateway.user.UserProfileGateway; 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.gateway.user.ability.RegisterDeviceGateway;
import com.red.circle.other.domain.model.user.UserProfile; import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.convertor.team.TeamInfraConvertor; import com.red.circle.other.infra.common.sys.CountryDataSyncLockService;
import com.red.circle.other.infra.common.user.UserCountryWriteLockService;
import com.red.circle.other.infra.convertor.team.TeamInfraConvertor;
import com.red.circle.other.infra.database.mongo.dto.team.TeamBillSettleResult; import com.red.circle.other.infra.database.mongo.dto.team.TeamBillSettleResult;
import com.red.circle.other.infra.database.mongo.entity.team.team.*; import com.red.circle.other.infra.database.mongo.entity.team.team.*;
import com.red.circle.other.infra.database.mongo.entity.user.region.SysRegionConfig; import com.red.circle.other.infra.database.mongo.entity.user.region.SysRegionConfig;
@ -37,9 +39,11 @@ import com.red.circle.other.infra.database.mongo.service.team.team.TeamNoticeSer
import com.red.circle.other.infra.database.mongo.service.team.team.TeamPolicyManagerService; import com.red.circle.other.infra.database.mongo.service.team.team.TeamPolicyManagerService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileService; import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamSalaryPaymentService; import com.red.circle.other.infra.database.mongo.service.team.team.TeamSalaryPaymentService;
import com.red.circle.other.infra.database.mongo.service.user.region.SysRegionConfigService; import com.red.circle.other.infra.database.mongo.service.user.region.SysRegionConfigService;
import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentBaseInfo; import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentTeam; import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentBaseInfo;
import com.red.circle.other.infra.database.rds.entity.team.BusinessDevelopmentTeam;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
import com.red.circle.other.infra.database.rds.service.team.*; import com.red.circle.other.infra.database.rds.service.team.*;
import com.red.circle.other.inner.asserts.team.TeamErrorCode; import com.red.circle.other.inner.asserts.team.TeamErrorCode;
import com.red.circle.other.inner.asserts.user.UserErrorCode; import com.red.circle.other.inner.asserts.user.UserErrorCode;
@ -83,12 +87,14 @@ import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.text.StringUtils; import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.wallet.inner.endpoint.bank.UserBankCardClient; import com.red.circle.wallet.inner.endpoint.bank.UserBankCardClient;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -129,6 +135,9 @@ public class TeamProfileClientServiceImpl implements TeamProfileClientService {
private final RegisterDeviceGateway registerDeviceGateway; private final RegisterDeviceGateway registerDeviceGateway;
private final UserMqMessageService userMqMessageService; private final UserMqMessageService userMqMessageService;
private final TeamManualSalaryPaymentClientService teamManualSalaryPaymentClientService; private final TeamManualSalaryPaymentClientService teamManualSalaryPaymentClientService;
private final SysCountryCodeService sysCountryCodeService;
private final CountryDataSyncLockService countryDataSyncLockService;
private final UserCountryWriteLockService userCountryWriteLockService;
@Override @Override
public List<TeamProfileDTO> listByIds(Set<Long> ids) { public List<TeamProfileDTO> listByIds(Set<Long> ids) {
@ -199,9 +208,19 @@ public class TeamProfileClientServiceImpl implements TeamProfileClientService {
} }
@Override @Override
public void create(TeamProfileCmd cmd) { public void create(TeamProfileCmd cmd) {
teamProfileService.create(teamInfraConvertor.toTeamProfile(cmd)); TeamProfile teamProfile = teamInfraConvertor.toTeamProfile(cmd);
Long countryId = teamProfile.getCountry() == null
? null : teamProfile.getCountry().getCountryId();
executeTeamCreationLocks(countryId, teamProfile.getOwnUserId(), () -> {
if (countryId != null) {
// 内部创建接口也可能携带旧国家快照稳定 countryId 对应的主表值才可落库.
teamProfile.setCountry(canonicalTeamCountry(countryId));
}
teamProfileService.create(teamProfile);
return null;
});
} }
@ -210,11 +229,68 @@ public class TeamProfileClientServiceImpl implements TeamProfileClientService {
teamApplicationProcessService.changeUserTeamWaitReject(userId); teamApplicationProcessService.changeUserTeamWaitReject(userId);
} }
@Override @Override
public void createTeam(TeamCreateCmd cmd) { public void createTeam(TeamCreateCmd cmd) {
UserProfile preflightUser = userProfileGateway.getByUserId(cmd.getOwnUserId());
UserProfile userProfile = userProfileGateway.getByUserId(cmd.getOwnUserId()); ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, preflightUser);
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, userProfile); Long expectedCountryId = preflightUser.getCountryId();
// 锁顺序固定为 country -> user与国家批量同步一致避免交叉死锁.
executeTeamCreationLocks(expectedCountryId, cmd.getOwnUserId(), () -> {
createTeamLocked(cmd, expectedCountryId);
return null;
});
}
private void createTeamLocked(TeamCreateCmd cmd, Long expectedCountryId) {
TeamCreationContext creationContext = createTeamProfile(cmd, expectedCountryId);
UserProfile userProfile = creationContext.userProfile();
TeamProfileDTO teamProfile = creationContext.teamProfile();
// 初始化账单
teamBillCycleService.createIfAbsent(
new TeamBillCmd()
.setSysOrigin(teamProfile.getSysOrigin())
.setTeamId(teamProfile.getId())
.setRegion(teamProfile.getRegion())
.setOperationTime(TimestampUtils.now())
.setOperationBackUser(teamProfile.getCreateUser())
);
teamMemberService.addMember(new TeamMember()
.setSortId(IdWorkerUtils.getId())
.setSysOrigin(teamProfile.getSysOrigin())
.setMemberId(userProfile.getId())
.setTeamId(teamProfile.getId())
.setRole(TeamMemberRole.OWN)
.setRemarks("")
.setCreateTime(TimestampUtils.now())
.setCreateUserOrigin(OperationUserOrigin.BACK.getValue())
.setCreateUser(cmd.getCreateUser())
.setUpdateTime(TimestampUtils.now())
.setUpdateUserOrigin(OperationUserOrigin.BACK.getValue())
.setUpdateUser(cmd.getCreateUser())
.setActiveTime(TimestampUtils.now())
);
// 处理等待消息, 全部标记拒绝
teamApplicationProcessService.changeUserTeamWaitReject(userProfile.getId());
// 标记身份
userHistoryIdentityService.saveAgentAndHost(userProfile.getId());
// 佩戴主播和代理徽章
userMqMessageService.sendBadgeOperateEvent(userProfile.getId(), BadgeKeyEnum.AGENCY.name(), BadgeOperateEvent.OperationType.WEAR.name());
userMqMessageService.sendBadgeOperateEvent(userProfile.getId(), BadgeKeyEnum.HOST.name(), BadgeOperateEvent.OperationType.WEAR.name());
redisService.unlock("CreateTeam:" + userProfile.getId());
}
private TeamCreationContext createTeamProfile(TeamCreateCmd cmd, Long expectedCountryId) {
UserProfile userProfile = userProfileGateway.getByUserId(cmd.getOwnUserId());
ResponseAssert.notNull(UserErrorCode.USER_INFO_NOT_FOUND, userProfile);
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT,
Objects.equals(expectedCountryId, userProfile.getCountryId()));
ResponseAssert.isTrue(UserErrorCode.USER_INFO_NOT_FOUND, ResponseAssert.isTrue(UserErrorCode.USER_INFO_NOT_FOUND,
Objects.equals(userProfile.getOriginSys(), cmd.getSysOrigin())); Objects.equals(userProfile.getOriginSys(), cmd.getSysOrigin()));
ResponseAssert.isFalse(TeamErrorCode.USER_ALREADY_TEAM_OWN, ResponseAssert.isFalse(TeamErrorCode.USER_ALREADY_TEAM_OWN,
@ -254,12 +330,7 @@ public class TeamProfileClientServiceImpl implements TeamProfileClientService {
); );
teamProfile.setRemarks(Lists.newArrayList()); teamProfile.setRemarks(Lists.newArrayList());
teamProfile.setSetting(cmd.getSetting()); teamProfile.setSetting(cmd.getSetting());
teamProfile.setCountry( teamProfile.setCountry(resolveTeamCountry(userProfile));
new TeamCountry()
.setCountryId(userProfile.getCountryId())
.setCountryCode(userProfile.getCountryCode())
.setCountryName(userProfile.getCountryName())
);
if (StringUtils.isBlank(teamProfile.getSysOrigin())) { if (StringUtils.isBlank(teamProfile.getSysOrigin())) {
teamProfile.setSysOrigin(userProfile.getOriginSys()); teamProfile.setSysOrigin(userProfile.getOriginSys());
@ -305,46 +376,38 @@ public class TeamProfileClientServiceImpl implements TeamProfileClientService {
businessDevelopmentBaseInfoService.incrMemberCount(Set.of(cmd.getBdUserId())); businessDevelopmentBaseInfoService.incrMemberCount(Set.of(cmd.getBdUserId()));
} }
// 创建团队 // 创建团队
teamProfileService.create(teamInfraConvertor.toTeamProfile(teamProfile)); teamProfileService.create(teamInfraConvertor.toTeamProfile(teamProfile));
return new TeamCreationContext(userProfile, teamProfile);
// 初始化账单 }
teamBillCycleService.createIfAbsent(
new TeamBillCmd() private TeamCountry resolveTeamCountry(UserProfile userProfile) {
.setSysOrigin(teamProfile.getSysOrigin()) if (userProfile.getCountryId() == null) {
.setTeamId(teamProfile.getId()) return new TeamCountry()
.setRegion(teamProfile.getRegion()) .setCountryCode(userProfile.getCountryCode())
.setOperationTime(TimestampUtils.now()) .setCountryName(userProfile.getCountryName());
.setOperationBackUser(teamProfile.getCreateUser()) }
); return canonicalTeamCountry(userProfile.getCountryId());
}
teamMemberService.addMember(new TeamMember()
.setSortId(IdWorkerUtils.getId()) private TeamCountry canonicalTeamCountry(Long countryId) {
.setSysOrigin(teamProfile.getSysOrigin()) // countryId 是稳定归属团队冗余码与名称始终取国家主表的最新标准值.
.setMemberId(userProfile.getId()) SysCountryCode country = sysCountryCodeService.getById(countryId);
.setTeamId(teamProfile.getId()) ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, country);
.setRole(TeamMemberRole.OWN) return new TeamCountry()
.setRemarks("") .setCountryId(country.getId())
.setCreateTime(TimestampUtils.now()) .setCountryCode(country.getAlphaTwo())
.setCreateUserOrigin(OperationUserOrigin.BACK.getValue()) .setCountryName(country.getCountryName());
.setCreateUser(cmd.getCreateUser()) }
.setUpdateTime(TimestampUtils.now())
.setUpdateUserOrigin(OperationUserOrigin.BACK.getValue()) private <T> T executeTeamCreationLocks(Long countryId, Long ownUserId, Supplier<T> action) {
.setUpdateUser(cmd.getCreateUser()) Supplier<T> userLocked = () -> userCountryWriteLockService.execute(
.setActiveTime(TimestampUtils.now()) ownUserId == null ? List.of() : List.of(ownUserId), action);
); return countryId == null ? userLocked.get()
: countryDataSyncLockService.execute(countryId, userLocked);
// 处理等待消息, 全部标记拒绝 }
teamApplicationProcessService.changeUserTeamWaitReject(userProfile.getId());
private record TeamCreationContext(UserProfile userProfile, TeamProfileDTO teamProfile) {
// 标记身份
userHistoryIdentityService.saveAgentAndHost(userProfile.getId());
// 佩戴主播和代理徽章
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 boolean existsAvailableTeamMember(Long userId) { private boolean existsAvailableTeamMember(Long userId) {
@ -373,17 +436,51 @@ public class TeamProfileClientServiceImpl implements TeamProfileClientService {
return businessDevelopmentBaseInfoService.getByUserId(param.getBdUserId()); return businessDevelopmentBaseInfoService.getByUserId(param.getBdUserId());
} }
@Override @Override
public void updateTeam(TeamUpdateCmd cmd) { public void updateTeam(TeamUpdateCmd cmd) {
TeamProfile oldTeamProfile = teamProfileService.getById(cmd.getId()); TeamProfile preflightTeam = teamProfileService.getById(cmd.getId());
ResponseAssert.notNull(TeamErrorCode.TEAM_NOT_FOUND, oldTeamProfile); ResponseAssert.notNull(TeamErrorCode.TEAM_NOT_FOUND, preflightTeam);
Long sourceCountryId = teamCountryId(preflightTeam.getCountry());
teamProfileService.updateByIdSelective(teamInfraConvertor.toTeamProfile(cmd)); Long requestedCountryId = teamCountryId(cmd.getCountry());
Long targetCountryId = requestedCountryId == null ? sourceCountryId : requestedCountryId;
if (!Objects.equals(cmd.getRegion(), oldTeamProfile.getRegion())) {
// 团队编辑与国家级联保持 country -> owner user 锁顺序阻止迁移前旧表单在完成后回写旧码.
SysRegionConfig newRegion = getSysRegionConfig(cmd.getRegion()); countryDataSyncLockService.execute(Arrays.asList(sourceCountryId, targetCountryId), () ->
userCountryWriteLockService.execute(
preflightTeam.getOwnUserId() == null ? List.of() : List.of(preflightTeam.getOwnUserId()),
() -> {
TeamProfile lockedTeam = teamProfileService.getById(cmd.getId());
ResponseAssert.notNull(TeamErrorCode.TEAM_NOT_FOUND, lockedTeam);
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT,
Objects.equals(sourceCountryId, teamCountryId(lockedTeam.getCountry())));
canonicalizeTeamCountry(cmd, targetCountryId);
updateTeamLocked(cmd, lockedTeam);
return null;
}));
}
private void canonicalizeTeamCountry(TeamUpdateCmd cmd, Long targetCountryId) {
if (targetCountryId == null) {
return;
}
// countryId 是团队国家归属编码和名称始终重新取国家主表不能信任页面旧快照.
cmd.setCountry(canonicalTeamCountry(targetCountryId));
}
private Long teamCountryId(TeamCountry country) {
return country == null ? null : country.getCountryId();
}
private void updateTeamLocked(TeamUpdateCmd cmd, TeamProfile oldTeamProfile) {
teamProfileService.updateByIdSelective(teamInfraConvertor.toTeamProfile(cmd));
String targetRegionId = StringUtils.isBlank(cmd.getRegion())
? oldTeamProfile.getRegion() : cmd.getRegion();
if (!Objects.equals(targetRegionId, oldTeamProfile.getRegion())) {
SysRegionConfig newRegion = getSysRegionConfig(targetRegionId);
SysRegionConfig oldRegion = getSysRegionConfig(oldTeamProfile.getRegion()); SysRegionConfig oldRegion = getSysRegionConfig(oldTeamProfile.getRegion());
// 将之前的账单修改为历史存档状态 // 将之前的账单修改为历史存档状态

View File

@ -8,19 +8,27 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import com.red.circle.framework.core.dto.PageCommand; import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.framework.core.dto.PageCommand;
import com.red.circle.framework.dto.PageResult; import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.service.user.user.UserAuthTypeService; import com.red.circle.other.app.service.user.user.UserAuthTypeService;
import com.red.circle.other.domain.model.user.UserProfile; import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.domain.gateway.user.UserProfileGateway; import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.infra.config.TimestampDeserializer; import com.red.circle.other.infra.common.sys.CountryDataSyncLockService;
import com.red.circle.other.infra.common.user.UserCountryWriteLockService;
import com.red.circle.other.infra.config.TimestampDeserializer;
import com.red.circle.other.infra.database.cache.service.user.UserCacheService; import com.red.circle.other.infra.database.cache.service.user.UserCacheService;
import com.red.circle.other.infra.database.cache.service.user.UserRegionCacheService; import com.red.circle.other.infra.database.cache.service.user.UserRegionCacheService;
import com.red.circle.other.infra.database.cache.service.user.UserRunProfileCacheService; import com.red.circle.other.infra.database.cache.service.user.UserRunProfileCacheService;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserRunProfileService; import com.red.circle.other.infra.database.mongo.service.user.profile.UserRunProfileService;
import com.red.circle.other.infra.database.rds.service.user.user.BaseInfoService; import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
import com.red.circle.other.infra.database.rds.service.user.user.BaseInfoService;
import com.red.circle.other.infra.database.rds.service.user.user.RegisterInfoService; import com.red.circle.other.infra.database.rds.service.user.user.RegisterInfoService;
import com.red.circle.other.infra.database.rds.service.user.user.RelationshipFriendService; import com.red.circle.other.infra.database.rds.service.user.user.RelationshipFriendService;
import com.red.circle.other.infra.database.rds.service.user.user.UserPhotoWallService; import com.red.circle.other.infra.database.rds.service.user.user.UserPhotoWallService;
@ -38,8 +46,8 @@ import com.red.circle.other.inner.model.dto.user.PetRelationshipFriendDTO;
import com.red.circle.other.inner.model.dto.user.UserPhotoWallDTO; import com.red.circle.other.inner.model.dto.user.UserPhotoWallDTO;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.other.inner.model.dto.user.UserRegisterInfoDTO; import com.red.circle.other.inner.model.dto.user.UserRegisterInfoDTO;
import com.red.circle.other.inner.model.dto.user.UserRunProfileDTO; import com.red.circle.other.inner.model.dto.user.UserRunProfileDTO;
import com.red.circle.other.app.inner.service.user.user.UserProfileClientService; import com.red.circle.other.app.inner.service.user.user.UserProfileClientService;
import java.io.IOException; import java.io.IOException;
import java.sql.Timestamp; import java.sql.Timestamp;
@ -70,8 +78,11 @@ public class UserProfileClientServiceImpl implements UserProfileClientService {
private final UserProfileInnerConvertor userProfileInnerConvertor; private final UserProfileInnerConvertor userProfileInnerConvertor;
private final RelationshipFriendService relationshipFriendService; private final RelationshipFriendService relationshipFriendService;
private final UserAuthTypeService userAuthTypeService; private final UserAuthTypeService userAuthTypeService;
private final UserRunProfileCacheService userRunProfileCacheService; private final UserRunProfileCacheService userRunProfileCacheService;
private final UserRegionCacheService userRegionCacheService; private final UserRegionCacheService userRegionCacheService;
private final SysCountryCodeService sysCountryCodeService;
private final CountryDataSyncLockService countryDataSyncLockService;
private final UserCountryWriteLockService userCountryWriteLockService;
@Override @Override
public void removeCacheAll(Long userId) { public void removeCacheAll(Long userId) {
@ -297,10 +308,36 @@ public class UserProfileClientServiceImpl implements UserProfileClientService {
} }
@Override @Override
public void updateSelectiveById(UserBaseInfoCmd cmd) { public void updateSelectiveById(UserBaseInfoCmd cmd) {
userProfileGateway.updateSelectiveById(userProfileInnerConvertor.toUserProfile(cmd)); boolean countrySubmitted = cmd.getCountryId() != null || cmd.getCountryCode() != null
} || cmd.getCountryName() != null;
if (!countrySubmitted || cmd.getId() == null) {
userProfileGateway.updateSelectiveById(userProfileInnerConvertor.toUserProfile(cmd));
return;
}
BaseInfo preflight = baseInfoService.getById(cmd.getId());
ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, preflight);
Long sourceCountryId = preflight.getCountryId();
Long targetCountryId = cmd.getCountryId() == null ? sourceCountryId : cmd.getCountryId();
ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR, targetCountryId);
// 后台单用户改国家与App保持相同 country -> user 锁顺序防止游标后迁入漏投影.
countryDataSyncLockService.execute(Arrays.asList(sourceCountryId, targetCountryId), () ->
userCountryWriteLockService.execute(List.of(cmd.getId()), () -> {
BaseInfo lockedUser = baseInfoService.getById(cmd.getId());
ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, lockedUser);
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT,
Objects.equals(sourceCountryId, lockedUser.getCountryId()));
SysCountryCode targetCountry = sysCountryCodeService.getById(targetCountryId);
ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_RECORD_INFO, targetCountry);
// 管理页可能提交迁移前打开的旧快照国家码和名称必须以稳定 countryId 的主表为准.
cmd.setCountryId(targetCountry.getId());
cmd.setCountryCode(targetCountry.getAlphaTwo());
cmd.setCountryName(targetCountry.getCountryName());
userProfileGateway.updateSelectiveById(userProfileInnerConvertor.toUserProfile(cmd));
return null;
}));
}
@Override @Override
public void restoreDelById(Long id) { public void restoreDelById(Long id) {

View File

@ -0,0 +1,30 @@
-- 国家资料级联同步按 (country_id, id) 游标分页;该索引避免国家更新时扫描整张用户表。
-- 生产表当前约 1.4 万行/4.5 MB使用 INPLACE + LOCK=NONE且限制元数据锁等待避免影响在线请求。
SET @current_schema = DATABASE();
SET SESSION lock_wait_timeout = 10;
SET @ddl = IF(
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS first_column
WHERE first_column.TABLE_SCHEMA = @current_schema
AND first_column.TABLE_NAME = 'user_base_info'
AND first_column.SEQ_IN_INDEX = 1
AND first_column.COLUMN_NAME = 'country_id'
AND EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS second_column
WHERE second_column.TABLE_SCHEMA = first_column.TABLE_SCHEMA
AND second_column.TABLE_NAME = first_column.TABLE_NAME
AND second_column.INDEX_NAME = first_column.INDEX_NAME
AND second_column.SEQ_IN_INDEX = 2
AND second_column.COLUMN_NAME = 'id'
)
),
'SELECT 1',
'ALTER TABLE `user_base_info` ADD KEY `idx_country_id_id` (`country_id`, `id`), ALGORITHM=INPLACE, LOCK=NONE'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,31 @@
// 国家资料级联同步索引批量迁移前执行createIndex 对同名同定义索引幂等。
db.getCollection("team_profile").createIndex(
{"country.countryId": 1, _id: 1}, {name: "idx_country_id_cursor"});
db.getCollection("team_member").createIndex(
{teamId: 1, memberId: 1}, {name: "idx_team_member"});
db.getCollection("team_member").createIndex(
{memberId: 1, teamId: 1}, {name: "idx_member_team"});
db.getCollection("active_voice_room").createIndex(
{userId: 1}, {name: "idx_user_id"});
db.getCollection("room_profile_manager").createIndex(
{userId: 1}, {name: "idx_user_id"});
db.getCollection("user_daily_active_log").createIndex(
{userId: 1, activeDate: 1}, {name: "idx_uid_date", unique: true});
db.getCollection("user_daily_active_log").createIndex(
{activeDate: 1, sysOrigin: 1}, {name: "idx_date_origin"});
db.getCollection("user_daily_active_log").createIndex(
{activeDate: 1, registerCountryCode: 1}, {name: "idx_date_country"});
db.getCollection("team_bill_cycle").createIndex(
{billBelong: 1, status: 1}, {name: "idx_billBelong_status"});
db.getCollection("team_bill_cycle").createIndex(
{teamId: 1, status: 1}, {name: "idx_team_status"});
db.getCollection("team_member_target").createIndex(
{teamId: 1, history: 1}, {name: "idx_team_history"});
db.getCollection("team_member_target").createIndex(
{userId: 1, history: 1}, {name: "idx_user_history"});
db.getCollection("agent_activity_count").createIndex(
{teamId: 1, history: 1}, {name: "idx_team_history"});