From 3168637944073edcbe8f2bc0d3b2268e46bacf73 Mon Sep 17 00:00:00 2001 From: hy001 Date: Fri, 22 May 2026 21:23:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=B2=E6=9C=89=E5=85=B3=E7=B3=BB=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=E5=85=B6=E4=BB=96=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/ImMessageClientServiceImpl.java | 4 +- .../app/command/user/ProcessCpApplyCmd.java | 93 +++++--- .../app/command/user/SendCpApplyCmdExe.java | 51 +---- .../common/cp/CpRelationC2cNoticeUtils.java | 205 ++++++++++++++++++ .../app/listener/user/CpApplyListener.java | 119 ++++++---- .../command/user/ProcessCpApplyCmdTest.java | 113 +++++++++- .../command/user/SendCpApplyCmdExeTest.java | 28 ++- .../listener/user/CpApplyListenerTest.java | 103 +++++++++ .../rds/service/user/user/CpApplyService.java | 16 +- .../user/user/CpRelationshipService.java | 11 +- .../user/user/impl/CpApplyServiceImpl.java | 66 +++++- .../user/impl/CpRelationshipServiceImpl.java | 23 +- 12 files changed, 696 insertions(+), 136 deletions(-) create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/cp/CpRelationC2cNoticeUtils.java create mode 100644 rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/listener/user/CpApplyListenerTest.java diff --git a/rc-service/rc-service-external/external-inner-endpoint/src/main/java/com/red/circle/external/inner/service/message/impl/ImMessageClientServiceImpl.java b/rc-service/rc-service-external/external-inner-endpoint/src/main/java/com/red/circle/external/inner/service/message/impl/ImMessageClientServiceImpl.java index 707089bc..3fc7109e 100644 --- a/rc-service/rc-service-external/external-inner-endpoint/src/main/java/com/red/circle/external/inner/service/message/impl/ImMessageClientServiceImpl.java +++ b/rc-service/rc-service-external/external-inner-endpoint/src/main/java/com/red/circle/external/inner/service/message/impl/ImMessageClientServiceImpl.java @@ -40,7 +40,9 @@ public class ImMessageClientServiceImpl implements ImMessageClientService { .msgRandom(DataTypeUtils.toInteger(NumUtils.getRandomNumberString(9))) .build() .addMsgBody(MessageBody.customBody(cmd.getDesc(), cmd.getData())); - imMessageManagerService.sendMessage(param).subscribe(); + imMessageManagerService.sendMessage(param) + .subscribe(res -> log.warn("sendCustomMessage response result:{}", res), + res -> log.error("sendCustomMessage error cmd={}, result={}", cmd, res)); } } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/ProcessCpApplyCmd.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/ProcessCpApplyCmd.java index 052ec829..e15e6bf3 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/ProcessCpApplyCmd.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/ProcessCpApplyCmd.java @@ -6,10 +6,12 @@ import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.core.response.CommonErrorCode; import com.red.circle.mq.business.model.event.user.CpApplyEvent; import com.red.circle.mq.rocket.business.producer.UserMqMessageService; +import com.red.circle.other.app.common.cp.CpRelationC2cNoticeUtils; import com.red.circle.other.app.common.cp.CpRelationBroadcastMqClient; import com.red.circle.other.app.dto.cmd.user.relation.cp.ProcessApplyCmd; -import com.red.circle.other.domain.gateway.user.UserProfileGateway; -import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.model.user.UserProfile; +import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService; import com.red.circle.other.infra.database.rds.entity.user.user.CpApply; import com.red.circle.other.infra.database.rds.entity.user.user.CpRelationship; import com.red.circle.other.infra.database.rds.entity.user.user.CpValue; @@ -22,10 +24,11 @@ import com.red.circle.tool.core.date.TimestampUtils; import com.red.circle.other.inner.asserts.user.UserRelationErrorCode; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; -import com.red.circle.wallet.inner.model.enums.GoldOrigin; -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Objects; +import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -67,17 +70,22 @@ public class ProcessCpApplyCmd { ResponseAssert.isTrue(CommonErrorCode.STATE_ERROR, Objects.equals(cpApply.getStatus(), CpApplyStatus.WAIT.name())); - // 检查是否是复合申请 - CpRelationship dismissingCp = cpRelationshipService.getDismissingCp( - cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId(), relationType); - boolean isReconcile = Objects.nonNull(dismissingCp); - - if (!isReconcile) { - // 非复合申请:CP已存在 + boolean agree = isAgree(cmd); + boolean isReconcile = false; + + if (agree) { + // 检查是否是复合申请 + CpRelationship dismissingCp = cpRelationshipService.getDismissingCp( + cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId(), relationType); + isReconcile = Objects.nonNull(dismissingCp); + } + + if (agree && !isReconcile) { + // 非复合申请:两人之间只能存在一种正常关系 ResponseAssert.isFalse(UserRelationErrorCode.ME_OR_YOU_CP_EXISTED, - cpRelationshipService.existsCp(cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId(), - relationType)); - + cpRelationshipService.existsRelationship(cpApply.getSendApplyUserId(), + cpApply.getAcceptApplyUserId())); + int maxCount = relationMaxCount(relationType); ResponseAssert.isFalse(UserRelationErrorCode.CP_LIMIT_EXCEEDED, cpRelationshipService.countCp(cpApply.getSendApplyUserId(), relationType) >= maxCount); @@ -86,13 +94,14 @@ public class ProcessCpApplyCmd { } // 操作失败 - ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE, - cpApplyService.changeStatusById(cmd.getApplyId(), getStatus(cmd))); - - if (!isAgree(cmd)) { - returnGoldCoins(cmd, cpApply); - return; - } + ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE, + cpApplyService.changeStatusById(cmd.getApplyId(), getStatus(cmd))); + + if (!agree) { + returnGoldCoins(cmd, cpApply); + sendProcessNotice(cpApply, false); + return; + } if (isReconcile) { // 复合:更新状态为NORMAL @@ -105,24 +114,52 @@ public class ProcessCpApplyCmd { cmd.requireReqSysOrigin(), relationType); } - refuseStatusWaitIfLimitReached(cmd, cpApply, relationType); + List refusedApplyIds = cpApplyService.refuseOtherWaitBetweenUsers(cpApply.getId(), + cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId()); + boolean refusedByLimit = refuseStatusWaitIfLimitReached(cmd, cpApply, relationType); + if (!refusedApplyIds.isEmpty() || refusedByLimit) { + scheduleReimburse(cmd, cpApply.getId()); + } cpRelationshipCacheService.remove(Arrays.asList(cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId())); cpRelationBroadcastMqClient.publish(cmd.requireReqSysOrigin(), cpApply.getId(), cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId(), relationType); + sendProcessNotice(cpApply, true); } - private void refuseStatusWaitIfLimitReached(ProcessApplyCmd cmd, CpApply cpApply, + private void sendProcessNotice(CpApply cpApply, boolean agree) { + try { + UserProfile senderProfile = userProfileGateway.getByUserId(cpApply.getSendApplyUserId()); + UserProfile acceptProfile = userProfileGateway.getByUserId(cpApply.getAcceptApplyUserId()); + if (Objects.isNull(senderProfile) || Objects.isNull(acceptProfile)) { + log.info("[CP] skip process c2c notice, applyId={}, senderProfile={}, acceptProfile={}", + cpApply.getId(), Objects.nonNull(senderProfile), Objects.nonNull(acceptProfile)); + return; + } + imMessageClient.sendCustomMessage(agree + ? CpRelationC2cNoticeUtils.buildAccepted(cpApply, senderProfile, acceptProfile) + : CpRelationC2cNoticeUtils.buildRejected(cpApply, senderProfile, acceptProfile)); + } catch (Exception ex) { + log.warn("[CP] send process c2c notice failed, applyId={}, agree={}, reason={}", + cpApply.getId(), agree, ex.getMessage(), ex); + } + } + + private boolean refuseStatusWaitIfLimitReached(ProcessApplyCmd cmd, CpApply cpApply, String relationType) { if (cpRelationshipService.countCp(cpApply.getAcceptApplyUserId(), relationType) < relationMaxCount(relationType)) { - return; + return false; } cpApplyService.refuseStatusWait(cmd.requiredReqUserId(), relationType); + return true; + } + + private void scheduleReimburse(ProcessApplyCmd cmd, Long applyId) { userMqMessageService.cpApplyDelayed( new CpApplyEvent() - .setId(cpApply.getId()) - .setSysOrigin(SysOriginPlatformEnum.valueOf(cmd.requireReqSysOrigin())), + .setId(applyId) + .setSysOrigin(SysOriginPlatformEnum.valueOf(cmd.requireReqSysOrigin())), TimestampUtils.nowPlusMinutes(1)); } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/SendCpApplyCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/SendCpApplyCmdExe.java index e4ff1362..c2f984af 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/SendCpApplyCmdExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/SendCpApplyCmdExe.java @@ -2,16 +2,15 @@ package com.red.circle.other.app.command.user; import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; import com.red.circle.external.inner.endpoint.message.ImMessageClient; -import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd; import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.core.response.CommonErrorCode; import com.red.circle.mq.business.model.event.user.CpApplyEvent; import com.red.circle.mq.rocket.business.producer.UserMqMessageService; +import com.red.circle.other.app.common.cp.CpRelationC2cNoticeUtils; import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; -import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyCmd; -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.ability.UserRegionGateway; +import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyCmd; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway; import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService; import com.red.circle.other.infra.database.rds.entity.user.user.CpApply; import com.red.circle.other.infra.database.rds.entity.user.user.CpRelationship; @@ -23,17 +22,12 @@ import com.red.circle.other.inner.asserts.user.UserRelationErrorCode; import com.red.circle.other.inner.enums.config.EnumConfigKey; import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.tool.core.date.TimestampUtils; -import com.red.circle.tool.core.json.JacksonUtils; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; import com.red.circle.wallet.inner.model.dto.WalletReceiptResDTO; import com.red.circle.wallet.inner.model.enums.GoldOrigin; - import java.math.BigDecimal; -import java.util.LinkedHashMap; -import java.util.Map; import java.util.Objects; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -48,7 +42,6 @@ import org.springframework.stereotype.Component; @Slf4j public class SendCpApplyCmdExe { - private static final String CP_BUILD_NOTICE_TYPE = "CP_BUILD"; private static final int CP_MAX_COUNT = 1; private static final int FRIEND_MAX_COUNT = 3; @@ -124,10 +117,9 @@ public class SendCpApplyCmdExe { boolean isReconcile = Objects.nonNull(dismissingCp); if (!isReconcile) { - // 非复合申请:检查CP是否已存在 + // 非复合申请:两人之间只能存在一种正常关系 ResponseAssert.isFalse(UserRelationErrorCode.ME_OR_YOU_CP_EXISTED, - cpRelationshipService - .existsCp(sendUserId, acceptUserId, relationType.name())); + cpRelationshipService.existsRelationship(sendUserId, acceptUserId)); validateRelationLimit(sendUserId, acceptUserId, relationType); } @@ -172,8 +164,8 @@ public class SendCpApplyCmdExe { cpApplyService.save(cpApply); - sendCpBuildIm(sendUserId, acceptUserId, cpApply.getId(), userProfile, isReconcile, - relationType, applyText); + sendCpBuildIm(sendUserId, acceptUserId, cpApply.getId(), userProfile, acceptUserProfile, + isReconcile, relationType, applyText); userMqMessageService.cpApplyDelayed( new CpApplyEvent() @@ -183,29 +175,10 @@ public class SendCpApplyCmdExe { } private void sendCpBuildIm(Long sendUserId, Long acceptUserId, Long applyId, - UserProfileDTO userProfile, boolean isReconcile, CpRelationshipType relationType, - String title) { - Map content = new LinkedHashMap<>(); - OfficialNoticeUtils.buildUserProfile(userProfile) - .forEach((key, value) -> content.put(String.valueOf(key), value)); - content.put("applyType", isReconcile ? "RECONCILE" : "APPLY"); - content.put("relationType", relationType.name()); - - Map payload = new LinkedHashMap<>(); - payload.put("noticeType", CP_BUILD_NOTICE_TYPE); - payload.put("type", CP_BUILD_NOTICE_TYPE); - payload.put("title", title); - payload.put("content", JacksonUtils.toJson(content)); - payload.put("expand", applyId); - payload.put("applyId", applyId); - payload.put("relationType", relationType.name()); - - imMessageClient.sendCustomMessage(CustomC2cMsgBodyCmd.builder() - .fromAccount(sendUserId) - .toAccount(acceptUserId) - .desc(CP_BUILD_NOTICE_TYPE) - .data(payload) - .build()); + UserProfileDTO userProfile, UserProfileDTO acceptUserProfile, boolean isReconcile, + CpRelationshipType relationType, String title) { + imMessageClient.sendCustomMessage(CpRelationC2cNoticeUtils.buildInvitePending(sendUserId, + acceptUserId, applyId, userProfile, acceptUserProfile, isReconcile, relationType, title)); } private BigDecimal normalizeApplyConsumeGold(BigDecimal applyConsumeGold) { diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/cp/CpRelationC2cNoticeUtils.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/cp/CpRelationC2cNoticeUtils.java new file mode 100644 index 00000000..4dddd1bd --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/cp/CpRelationC2cNoticeUtils.java @@ -0,0 +1,205 @@ +package com.red.circle.other.app.common.cp; + +import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd; +import com.red.circle.other.domain.model.user.UserProfile; +import com.red.circle.other.infra.database.rds.entity.user.user.CpApply; +import com.red.circle.other.infra.enums.user.user.CpRelationshipType; +import com.red.circle.other.inner.model.dto.user.UserProfileDTO; +import com.red.circle.tool.core.json.JacksonUtils; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; + +public final class CpRelationC2cNoticeUtils { + + public static final String NOTICE_TYPE = "CP_BUILD"; + public static final String ACTION_INVITE_PENDING = "INVITE_PENDING"; + public static final String ACTION_RELATION_ESTABLISHED = "RELATION_ESTABLISHED"; + public static final String ACTION_RELATION_REJECTED = "RELATION_REJECTED"; + public static final String ACTION_RELATION_EXPIRED = "RELATION_EXPIRED"; + + private CpRelationC2cNoticeUtils() { + } + + public static CustomC2cMsgBodyCmd buildInvitePending(Long fromUserId, Long toUserId, + Long applyId, UserProfileDTO senderProfile, UserProfileDTO acceptProfile, + boolean isReconcile, CpRelationshipType relationType, String title) { + Map content = senderContent(senderProfile); + content.put("applyType", isReconcile ? "RECONCILE" : "APPLY"); + content.put("relationType", relationType.name()); + putAcceptProfile(content, acceptProfile); + + Map payload = basePayload(applyId, relationType.name(), title, + ACTION_INVITE_PENDING, "PENDING", content); + putSenderProfile(payload, senderProfile); + putAcceptProfile(payload, acceptProfile); + return buildMessage(fromUserId, toUserId, payload); + } + + public static CustomC2cMsgBodyCmd buildAccepted(CpApply cpApply, UserProfile senderProfile, + UserProfile acceptProfile) { + String relationType = CpRelationshipType.normalize(cpApply.getRelationType()); + String title = "User " + displayName(acceptProfile) + " accepted your " + + relationType.toLowerCase() + " invitation."; + Map content = senderContent(senderProfile); + content.put("applyType", "APPLY"); + content.put("relationType", relationType); + putAcceptProfile(content, acceptProfile); + + Map payload = basePayload(cpApply.getId(), relationType, title, + ACTION_RELATION_ESTABLISHED, "ACCEPTED", content); + payload.put("agree", true); + putSenderProfile(payload, senderProfile); + putAcceptProfile(payload, acceptProfile); + return buildMessage(cpApply.getAcceptApplyUserId(), cpApply.getSendApplyUserId(), payload); + } + + public static CustomC2cMsgBodyCmd buildRejected(CpApply cpApply, UserProfile senderProfile, + UserProfile acceptProfile) { + String relationType = CpRelationshipType.normalize(cpApply.getRelationType()); + String title = "User " + displayName(acceptProfile) + " declined your " + + relationType.toLowerCase() + " invitation."; + Map content = senderContent(senderProfile); + content.put("applyType", "APPLY"); + content.put("relationType", relationType); + putAcceptProfile(content, acceptProfile); + + Map payload = basePayload(cpApply.getId(), relationType, title, + ACTION_RELATION_REJECTED, "REJECTED", content); + payload.put("agree", false); + putSenderProfile(payload, senderProfile); + putAcceptProfile(payload, acceptProfile); + return buildMessage(cpApply.getAcceptApplyUserId(), cpApply.getSendApplyUserId(), payload); + } + + public static CustomC2cMsgBodyCmd buildExpiredToAcceptUser(CpApply cpApply, + UserProfile senderProfile, UserProfile acceptProfile) { + return buildExpired(cpApply, senderProfile, acceptProfile, cpApply.getSendApplyUserId(), + cpApply.getAcceptApplyUserId()); + } + + public static CustomC2cMsgBodyCmd buildExpiredToSendUser(CpApply cpApply, + UserProfile senderProfile, UserProfile acceptProfile) { + return buildExpired(cpApply, senderProfile, acceptProfile, cpApply.getAcceptApplyUserId(), + cpApply.getSendApplyUserId()); + } + + private static CustomC2cMsgBodyCmd buildExpired(CpApply cpApply, UserProfile senderProfile, + UserProfile acceptProfile, Long fromUserId, Long toUserId) { + String relationType = CpRelationshipType.normalize(cpApply.getRelationType()); + String title = "Your " + relationType.toLowerCase() + " invitation expired."; + Map content = senderContent(senderProfile); + content.put("applyType", "APPLY"); + content.put("relationType", relationType); + putAcceptProfile(content, acceptProfile); + + Map payload = basePayload(cpApply.getId(), relationType, title, + ACTION_RELATION_EXPIRED, "EXPIRED", content); + payload.put("expired", true); + putSenderProfile(payload, senderProfile); + putAcceptProfile(payload, acceptProfile); + return buildMessage(fromUserId, toUserId, payload); + } + + private static Map basePayload(Long applyId, String relationType, String title, + String action, String status, Map content) { + Map payload = new LinkedHashMap<>(); + payload.put("noticeType", NOTICE_TYPE); + payload.put("type", NOTICE_TYPE); + payload.put("noticeAction", action); + payload.put("action", action); + payload.put("status", status); + payload.put("result", status); + payload.put("state", status); + payload.put("title", title); + payload.put("content", JacksonUtils.toJson(content)); + payload.put("expand", applyId); + payload.put("applyId", applyId); + payload.put("relationType", relationType); + return payload; + } + + private static CustomC2cMsgBodyCmd buildMessage(Long fromUserId, Long toUserId, + Map payload) { + return CustomC2cMsgBodyCmd.builder() + .fromAccount(fromUserId) + .toAccount(toUserId) + .desc(NOTICE_TYPE) + .data(payload) + .build(); + } + + private static Map senderContent(UserProfileDTO userProfile) { + Map content = new LinkedHashMap<>(); + content.put("userNickname", userProfile.getUserNickname()); + content.put("account", userProfile.getAccount()); + content.put("actualAccount", userProfile.actualAccount()); + content.put("userAvatar", userProfile.getUserAvatar()); + content.put("countryCode", userProfile.getCountryCode()); + content.put("countryName", userProfile.getCountryName()); + return content; + } + + private static Map senderContent(UserProfile userProfile) { + Map content = new LinkedHashMap<>(); + content.put("userNickname", Objects.nonNull(userProfile) ? userProfile.getUserNickname() : null); + content.put("account", Objects.nonNull(userProfile) ? userProfile.getAccount() : null); + content.put("actualAccount", Objects.nonNull(userProfile) ? userProfile.getActualAccount() : null); + content.put("userAvatar", Objects.nonNull(userProfile) ? userProfile.getUserAvatar() : null); + content.put("countryCode", Objects.nonNull(userProfile) ? userProfile.getCountryCode() : null); + content.put("countryName", Objects.nonNull(userProfile) ? userProfile.getCountryName() : null); + return content; + } + + private static void putSenderProfile(Map payload, UserProfileDTO userProfile) { + payload.put("account", userProfile.getAccount()); + payload.put("actualAccount", userProfile.actualAccount()); + payload.put("userNickname", userProfile.getUserNickname()); + payload.put("userAvatar", userProfile.getUserAvatar()); + } + + private static void putSenderProfile(Map payload, UserProfile userProfile) { + if (Objects.isNull(userProfile)) { + return; + } + payload.put("account", userProfile.getAccount()); + payload.put("actualAccount", userProfile.getActualAccount()); + payload.put("userNickname", userProfile.getUserNickname()); + payload.put("userAvatar", userProfile.getUserAvatar()); + } + + private static void putAcceptProfile(Map payload, UserProfileDTO userProfile) { + payload.put("acceptUserId", userProfile.getId()); + payload.put("acceptAccount", userProfile.getAccount()); + payload.put("acceptActualAccount", userProfile.actualAccount()); + payload.put("acceptNickname", userProfile.getUserNickname()); + payload.put("acceptUserNickname", userProfile.getUserNickname()); + payload.put("acceptUserAvatar", userProfile.getUserAvatar()); + } + + private static void putAcceptProfile(Map payload, UserProfile userProfile) { + if (Objects.isNull(userProfile)) { + return; + } + payload.put("acceptUserId", userProfile.getId()); + payload.put("acceptAccount", userProfile.getAccount()); + payload.put("acceptActualAccount", userProfile.getActualAccount()); + payload.put("acceptNickname", userProfile.getUserNickname()); + payload.put("acceptUserNickname", userProfile.getUserNickname()); + payload.put("acceptUserAvatar", userProfile.getUserAvatar()); + } + + private static String displayName(UserProfile userProfile) { + if (Objects.isNull(userProfile)) { + return ""; + } + if (StringUtils.isNotBlank(userProfile.getUserNickname())) { + return userProfile.getUserNickname(); + } + if (StringUtils.isNotBlank(userProfile.getAccount())) { + return userProfile.getAccount(); + } + return Objects.toString(userProfile.getId(), ""); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/listener/user/CpApplyListener.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/listener/user/CpApplyListener.java index 4916a221..7f8b7add 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/listener/user/CpApplyListener.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/listener/user/CpApplyListener.java @@ -1,23 +1,29 @@ package com.red.circle.other.app.listener.user; -import com.red.circle.component.mq.MessageEventProcess; -import com.red.circle.component.mq.MessageEventProcessDescribe; -import com.red.circle.component.mq.config.RocketMqMessageListener; -import com.red.circle.component.mq.service.Action; -import com.red.circle.component.mq.service.ConsumerMessage; -import com.red.circle.component.mq.service.MessageListener; -import com.red.circle.mq.business.model.event.user.CpApplyEvent; -import com.red.circle.mq.rocket.business.streams.CpApplySink; -import com.red.circle.tool.core.collection.CollectionUtils; -import com.red.circle.tool.core.json.JacksonUtils; -import com.red.circle.other.infra.database.rds.entity.user.user.CpApply; +import com.red.circle.component.mq.MessageEventProcess; +import com.red.circle.component.mq.MessageEventProcessDescribe; +import com.red.circle.component.mq.config.RocketMqMessageListener; +import com.red.circle.component.mq.service.Action; +import com.red.circle.component.mq.service.ConsumerMessage; +import com.red.circle.component.mq.service.MessageListener; +import com.red.circle.external.inner.endpoint.message.ImMessageClient; +import com.red.circle.mq.business.model.event.user.CpApplyEvent; +import com.red.circle.mq.rocket.business.streams.CpApplySink; +import com.red.circle.other.app.common.cp.CpRelationC2cNoticeUtils; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.model.user.UserProfile; +import com.red.circle.tool.core.collection.CollectionUtils; +import com.red.circle.tool.core.json.JacksonUtils; +import com.red.circle.other.infra.database.rds.entity.user.user.CpApply; import com.red.circle.other.infra.database.rds.service.user.user.CpApplyService; import com.red.circle.other.infra.enums.user.user.CpApplyStatus; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; -import com.red.circle.wallet.inner.model.enums.GoldOrigin; -import java.util.List; -import java.util.Objects; +import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,11 +35,13 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor @RocketMqMessageListener(topic = "${rocketmq.topics.RC_DEFAULT_APP_DELAYED}", groupId = CpApplySink.INPUT, tag = CpApplySink.TAG) -public class CpApplyListener implements MessageListener { - - private final CpApplyService cpApplyService; - private final WalletGoldClient walletGoldClient; - private final MessageEventProcess messageEventProcess; +public class CpApplyListener implements MessageListener { + + private final CpApplyService cpApplyService; + private final ImMessageClient imMessageClient; + private final UserProfileGateway userProfileGateway; + private final WalletGoldClient walletGoldClient; + private final MessageEventProcess messageEventProcess; @Override public Action consume(ConsumerMessage message) { @@ -59,22 +67,38 @@ public class CpApplyListener implements MessageListener { return; } - // 等待超时 - if (Objects.equals(cpApply.getStatus(), CpApplyStatus.WAIT.name())) { - reimburse(eventBody, cpApply); - } - }); - } + // 等待超时 + if (Objects.equals(cpApply.getStatus(), CpApplyStatus.WAIT.name())) { + reimburse(eventBody, cpApply, true); + } + }); + } - private void checkReimburse(CpApplyEvent param, CpApply cpApply) { - List cpApplies = cpApplyService.listRefuse(cpApply.getAcceptApplyUserId()); - if (CollectionUtils.isEmpty(cpApplies)) { - return; - } - cpApplies.forEach(apply -> reimburse(param, apply)); - } - - private void reimburse(CpApplyEvent param, CpApply cpApply) { + private void checkReimburse(CpApplyEvent param, CpApply cpApply) { + Map applyMap = new LinkedHashMap<>(); + addReimburseApplies(applyMap, cpApplyService.listRefuse(cpApply.getAcceptApplyUserId())); + addReimburseApplies(applyMap, cpApplyService.listRefuseBetweenUsers(cpApply.getSendApplyUserId(), + cpApply.getAcceptApplyUserId())); + if (applyMap.isEmpty()) { + return; + } + applyMap.values().forEach(apply -> reimburse(param, apply, false)); + } + + private void addReimburseApplies(Map applyMap, List cpApplies) { + if (CollectionUtils.isEmpty(cpApplies)) { + return; + } + cpApplies.stream() + .filter(Objects::nonNull) + .filter(apply -> Objects.nonNull(apply.getId())) + .forEach(apply -> applyMap.putIfAbsent(apply.getId(), apply)); + } + + private void reimburse(CpApplyEvent param, CpApply cpApply, boolean expired) { + if (expired) { + sendExpiredNotice(cpApply); + } if (Objects.isNull(cpApply.getApplyConsumeGold()) || cpApply.getApplyConsumeGold().signum() <= 0) { cpApplyService.changeStatusById(cpApply.getId(), CpApplyStatus.REIMBURSE); @@ -88,7 +112,26 @@ public class CpApplyListener implements MessageListener { .origin(GoldOrigin.CP_BUILD) .amount(cpApply.getApplyConsumeGold()) .build()); - cpApplyService.changeStatusById(cpApply.getId(), CpApplyStatus.REIMBURSE); - } - -} + cpApplyService.changeStatusById(cpApply.getId(), CpApplyStatus.REIMBURSE); + } + + private void sendExpiredNotice(CpApply cpApply) { + try { + UserProfile senderProfile = userProfileGateway.getByUserId(cpApply.getSendApplyUserId()); + UserProfile acceptProfile = userProfileGateway.getByUserId(cpApply.getAcceptApplyUserId()); + if (Objects.isNull(senderProfile) || Objects.isNull(acceptProfile)) { + log.info("[CP] skip expired c2c notice, applyId={}, senderProfile={}, acceptProfile={}", + cpApply.getId(), Objects.nonNull(senderProfile), Objects.nonNull(acceptProfile)); + return; + } + imMessageClient.sendCustomMessage( + CpRelationC2cNoticeUtils.buildExpiredToAcceptUser(cpApply, senderProfile, acceptProfile)); + imMessageClient.sendCustomMessage( + CpRelationC2cNoticeUtils.buildExpiredToSendUser(cpApply, senderProfile, acceptProfile)); + } catch (Exception ex) { + log.warn("[CP] send expired c2c notice failed, applyId={}, reason={}", cpApply.getId(), + ex.getMessage(), ex); + } + } + +} diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/ProcessCpApplyCmdTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/ProcessCpApplyCmdTest.java index b09e9366..f76cf722 100644 --- a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/ProcessCpApplyCmdTest.java +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/ProcessCpApplyCmdTest.java @@ -1,17 +1,23 @@ package com.red.circle.other.app.command.user; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.red.circle.external.inner.endpoint.message.ImMessageClient; +import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd; import com.red.circle.framework.core.dto.ReqSysOrigin; +import com.red.circle.framework.dto.ResultResponse; +import com.red.circle.mq.business.model.event.user.CpApplyEvent; import com.red.circle.mq.rocket.business.producer.UserMqMessageService; import com.red.circle.other.app.common.cp.CpRelationBroadcastMqClient; import com.red.circle.other.app.dto.cmd.user.relation.cp.ProcessApplyCmd; import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.model.user.UserProfile; import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService; import com.red.circle.other.infra.database.rds.entity.user.user.CpApply; import com.red.circle.other.infra.database.rds.service.user.user.CpApplyService; @@ -19,7 +25,12 @@ import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipS import com.red.circle.other.infra.database.rds.service.user.user.CpValueService; import com.red.circle.other.infra.enums.user.user.CpApplyStatus; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; class ProcessCpApplyCmdTest { @@ -27,30 +38,75 @@ class ProcessCpApplyCmdTest { void agreeBrotherApplyShouldCreateBrotherRelationship() { Fixture fixture = new Fixture("BROTHER"); - fixture.exe.execute(processCmd()); + fixture.exe.execute(processCmd(true)); verify(fixture.cpRelationshipService).addCp(eq(1001L), eq(2001L), nullable(Long.class), eq("LIKEI"), eq("BROTHER")); verify(fixture.cpRelationBroadcastMqClient).publish("LIKEI", 3001L, 1001L, 2001L, "BROTHER"); + assertProcessMessage(fixture.sentCustomMessage(), "RELATION_ESTABLISHED", "ACCEPTED", + true, "BROTHER"); } @Test void agreeSistersApplyShouldCreateSistersRelationship() { Fixture fixture = new Fixture("SISTERS"); - fixture.exe.execute(processCmd()); + fixture.exe.execute(processCmd(true)); verify(fixture.cpRelationshipService).addCp(eq(1001L), eq(2001L), nullable(Long.class), eq("LIKEI"), eq("SISTERS")); verify(fixture.cpRelationBroadcastMqClient).publish("LIKEI", 3001L, 1001L, 2001L, "SISTERS"); + assertProcessMessage(fixture.sentCustomMessage(), "RELATION_ESTABLISHED", "ACCEPTED", + true, "SISTERS"); } - private static ProcessApplyCmd processCmd() { + @Test + void rejectApplyShouldSendRejectedC2cMessageToSender() { + Fixture fixture = new Fixture("CP"); + + fixture.exe.execute(processCmd(false)); + + assertProcessMessage(fixture.sentCustomMessage(), "RELATION_REJECTED", "REJECTED", + false, "CP"); + } + + @Test + void agreeApplyShouldFailWhenPairAlreadyHasAnyRelation() { + Fixture fixture = new Fixture("BROTHER"); + when(fixture.cpRelationshipService.existsRelationship(1001L, 2001L)).thenReturn(true); + + Assertions.assertThrows(RuntimeException.class, () -> fixture.exe.execute(processCmd(true))); + + verify(fixture.cpApplyService, never()).changeStatusById(eq(3001L), + any(CpApplyStatus.class)); + verify(fixture.cpRelationshipService, never()).addCp(any(Long.class), any(Long.class), + nullable(Long.class), any(String.class), any(String.class)); + verify(fixture.cpRelationBroadcastMqClient, never()).publish(any(String.class), + any(Long.class), any(Long.class), any(Long.class), any(String.class)); + verify(fixture.imMessageClient, never()).sendCustomMessage(any(CustomC2cMsgBodyCmd.class)); + } + + @Test + void agreeApplyShouldRejectOtherWaitAppliesBetweenSameUsers() { + Fixture fixture = new Fixture("CP"); + when(fixture.cpApplyService.refuseOtherWaitBetweenUsers(3001L, 1001L, 2001L)) + .thenReturn(List.of(3002L, 3003L)); + + fixture.exe.execute(processCmd(true)); + + verify(fixture.cpApplyService).refuseOtherWaitBetweenUsers(3001L, 1001L, 2001L); + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(CpApplyEvent.class); + verify(fixture.userMqMessageService).cpApplyDelayed(eventCaptor.capture(), + any(Timestamp.class)); + Assertions.assertEquals(3001L, eventCaptor.getValue().getId()); + } + + private static ProcessApplyCmd processCmd(boolean agree) { ProcessApplyCmd cmd = new ProcessApplyCmd() .setApplyId(3001L) - .setAgree(Boolean.TRUE); + .setAgree(agree); cmd.setReqUserId(2001L); cmd.setReqSysOrigin(ReqSysOrigin.of("LIKEI", "LIKEI")); return cmd; @@ -65,25 +121,53 @@ class ProcessCpApplyCmdTest { .setStatus(CpApplyStatus.WAIT.name()); } + @SuppressWarnings("unchecked") + private static void assertProcessMessage(CustomC2cMsgBodyCmd message, String action, + String status, Boolean agree, String relationType) { + Assertions.assertEquals("2001", message.getFromAccount()); + Assertions.assertEquals("1001", message.getToAccount()); + Assertions.assertEquals("CP_BUILD", message.getDesc()); + Map data = (Map) message.getData(); + Assertions.assertEquals("CP_BUILD", data.get("noticeType")); + Assertions.assertEquals(action, data.get("noticeAction")); + Assertions.assertEquals(action, data.get("action")); + Assertions.assertEquals(status, data.get("status")); + Assertions.assertEquals(status, data.get("result")); + Assertions.assertEquals(status, data.get("state")); + Assertions.assertEquals(agree, data.get("agree")); + Assertions.assertEquals(3001L, data.get("applyId")); + Assertions.assertEquals(relationType, data.get("relationType")); + Assertions.assertTrue(data.get("content").toString() + .contains("\"relationType\":\"" + relationType + "\"")); + } + private static class Fixture { private final CpApplyService cpApplyService = mock(CpApplyService.class); private final CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); private final CpRelationBroadcastMqClient cpRelationBroadcastMqClient = mock( CpRelationBroadcastMqClient.class); + private final ImMessageClient imMessageClient = mock(ImMessageClient.class); + private final UserMqMessageService userMqMessageService = mock(UserMqMessageService.class); private final ProcessCpApplyCmd exe; private Fixture(String relationType) { CpValueService cpValueService = mock(CpValueService.class); - ImMessageClient imMessageClient = mock(ImMessageClient.class); WalletGoldClient walletGoldClient = mock(WalletGoldClient.class); - UserMqMessageService userMqMessageService = mock(UserMqMessageService.class); UserProfileGateway userProfileGateway = mock(UserProfileGateway.class); CpRelationshipCacheService cpRelationshipCacheService = mock( CpRelationshipCacheService.class); when(cpApplyService.getById(3001L)).thenReturn(apply(relationType)); when(cpApplyService.changeStatusById(3001L, CpApplyStatus.AGREE)).thenReturn(true); + when(cpApplyService.changeStatusById(3001L, CpApplyStatus.REFUSE)).thenReturn(true); + when(cpApplyService.changeStatusById(3001L, CpApplyStatus.REIMBURSE)).thenReturn(true); + when(cpApplyService.refuseOtherWaitBetweenUsers(3001L, 1001L, 2001L)) + .thenReturn(List.of()); + when(userProfileGateway.getByUserId(1001L)).thenReturn(profile(1001L)); + when(userProfileGateway.getByUserId(2001L)).thenReturn(profile(2001L)); + when(imMessageClient.sendCustomMessage(any(CustomC2cMsgBodyCmd.class))) + .thenReturn(ResultResponse.success()); exe = new ProcessCpApplyCmd( cpApplyService, @@ -96,5 +180,22 @@ class ProcessCpApplyCmdTest { cpRelationshipCacheService, cpRelationBroadcastMqClient); } + + private CustomC2cMsgBodyCmd sentCustomMessage() { + ArgumentCaptor captor = ArgumentCaptor.forClass( + CustomC2cMsgBodyCmd.class); + verify(imMessageClient).sendCustomMessage(captor.capture()); + return captor.getValue(); + } + + private static UserProfile profile(Long userId) { + UserProfile userProfile = new UserProfile(); + userProfile.setId(userId); + userProfile.setAccount(userId.toString()); + userProfile.setUserNickname("user-" + userId); + userProfile.setCountryCode("SA"); + userProfile.setCountryName("Saudi Arabia"); + return userProfile; + } } } diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/SendCpApplyCmdExeTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/SendCpApplyCmdExeTest.java index 05e7b11c..3a510efc 100644 --- a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/SendCpApplyCmdExeTest.java +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/SendCpApplyCmdExeTest.java @@ -13,6 +13,7 @@ import static org.mockito.Mockito.when; import com.red.circle.external.inner.endpoint.message.ImMessageClient; import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd; import com.red.circle.framework.dto.ResultResponse; +import com.red.circle.mq.business.model.event.user.CpApplyEvent; import com.red.circle.mq.rocket.business.producer.UserMqMessageService; import com.red.circle.other.app.convertor.user.UserProfileAppConvertor; import com.red.circle.other.domain.gateway.user.UserProfileGateway; @@ -24,6 +25,7 @@ import com.red.circle.other.infra.database.rds.service.user.user.CpApplyService; import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipService; import com.red.circle.other.inner.model.dto.user.UserProfileDTO; import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; +import java.sql.Timestamp; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -52,6 +54,27 @@ class SendCpApplyCmdExeTest { verify(fixture.cpApplyService, never()).save(any(CpApply.class)); } + @Test + void autoCreateFromCpGiftShouldSkipWhenAnyRelationAlreadyExistsInEitherDirection() { + Fixture fixture = new Fixture(1, 0); + when(fixture.cpRelationshipService.existsRelationship(1001L, 2001L)).thenReturn(true); + + assertFalse(fixture.exe.autoCreateFromCpGift(1001L, 2001L, "LIKEI", "BROTHER")); + verify(fixture.cpApplyService, never()).save(any(CpApply.class)); + verify(fixture.imMessageClient, never()).sendCustomMessage(any(CustomC2cMsgBodyCmd.class)); + verify(fixture.userMqMessageService, never()).cpApplyDelayed(any(CpApplyEvent.class), + any(Timestamp.class)); + + Fixture reverseFixture = new Fixture(1, 0); + when(reverseFixture.cpRelationshipService.existsRelationship(2001L, 1001L)).thenReturn(true); + + assertFalse(reverseFixture.exe.autoCreateFromCpGift(2001L, 1001L, "LIKEI", "SISTERS")); + verify(reverseFixture.cpApplyService, never()).save(any(CpApply.class)); + verify(reverseFixture.imMessageClient, never()).sendCustomMessage(any(CustomC2cMsgBodyCmd.class)); + verify(reverseFixture.userMqMessageService, never()).cpApplyDelayed(any(CpApplyEvent.class), + any(Timestamp.class)); + } + @Test void autoCreateFromCpGiftShouldCreateIndependentAppliesForDifferentRelationTypes() { Fixture fixture = new Fixture(1, 0); @@ -90,13 +113,13 @@ class SendCpApplyCmdExeTest { private final CpApplyService cpApplyService = mock(CpApplyService.class); private final ImMessageClient imMessageClient = mock(ImMessageClient.class); + private final UserMqMessageService userMqMessageService = mock(UserMqMessageService.class); + private final CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); private final SendCpApplyCmdExe exe; private long nextApplyId = 3001L; private Fixture(Integer sendSex, Integer acceptSex) { WalletGoldClient walletGoldClient = mock(WalletGoldClient.class); - UserMqMessageService userMqMessageService = mock(UserMqMessageService.class); - CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); EnumConfigCacheService enumConfigCacheService = mock(EnumConfigCacheService.class); UserProfileGateway userProfileGateway = mock(UserProfileGateway.class); UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class); @@ -110,6 +133,7 @@ class SendCpApplyCmdExeTest { UserProfileDTO acceptProfileDTO = profile(2001L, acceptSex); when(userRegionGateway.checkEqRegion(1001L, 2001L)).thenReturn(true); + when(userRegionGateway.checkEqRegion(2001L, 1001L)).thenReturn(true); when(userProfileGateway.getByUserId(1001L)).thenReturn(sendProfile); when(userProfileGateway.getByUserId(2001L)).thenReturn(acceptProfile); when(userProfileAppConvertor.toUserProfileDTO(sendProfile)).thenReturn(sendProfileDTO); diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/listener/user/CpApplyListenerTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/listener/user/CpApplyListenerTest.java new file mode 100644 index 00000000..d9bd2ff1 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/listener/user/CpApplyListenerTest.java @@ -0,0 +1,103 @@ +package com.red.circle.other.app.listener.user; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; +import com.red.circle.component.mq.MessageEventProcess; +import com.red.circle.component.mq.MessageEventProcessDescribe; +import com.red.circle.component.mq.service.Action; +import com.red.circle.component.mq.service.ConsumerMessage; +import com.red.circle.external.inner.endpoint.message.ImMessageClient; +import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd; +import com.red.circle.framework.dto.ResultResponse; +import com.red.circle.mq.business.model.event.user.CpApplyEvent; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.model.user.UserProfile; +import com.red.circle.other.infra.database.rds.entity.user.user.CpApply; +import com.red.circle.other.infra.database.rds.service.user.user.CpApplyService; +import com.red.circle.other.infra.enums.user.user.CpApplyStatus; +import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class CpApplyListenerTest { + + @Test + void waitApplyExpiredShouldSendC2cMessageToBothUsers() { + CpApplyService cpApplyService = mock(CpApplyService.class); + ImMessageClient imMessageClient = mock(ImMessageClient.class); + UserProfileGateway userProfileGateway = mock(UserProfileGateway.class); + WalletGoldClient walletGoldClient = mock(WalletGoldClient.class); + MessageEventProcess messageEventProcess = mock(MessageEventProcess.class); + CpApply apply = new CpApply() + .setId(3001L) + .setSendApplyUserId(1001L) + .setAcceptApplyUserId(2001L) + .setRelationType("SISTERS") + .setStatus(CpApplyStatus.WAIT.name()); + + when(cpApplyService.getById(3001L)).thenReturn(apply); + when(cpApplyService.changeStatusById(3001L, CpApplyStatus.REIMBURSE)).thenReturn(true); + when(userProfileGateway.getByUserId(1001L)).thenReturn(profile(1001L)); + when(userProfileGateway.getByUserId(2001L)).thenReturn(profile(2001L)); + when(imMessageClient.sendCustomMessage(any(CustomC2cMsgBodyCmd.class))) + .thenReturn(ResultResponse.success()); + when(messageEventProcess.consume(any(MessageEventProcessDescribe.class), + eq(CpApplyEvent.class), any())).thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + Consumer consumer = invocation.getArgument(2, Consumer.class); + consumer.accept(new CpApplyEvent() + .setId(3001L) + .setSysOrigin(SysOriginPlatformEnum.LIKEI)); + return Action.SUCCESS; + }); + + CpApplyListener listener = new CpApplyListener(cpApplyService, imMessageClient, + userProfileGateway, walletGoldClient, messageEventProcess); + + assertEquals(Action.SUCCESS, listener.consume(new ConsumerMessage())); + + ArgumentCaptor captor = ArgumentCaptor.forClass( + CustomC2cMsgBodyCmd.class); + verify(imMessageClient, org.mockito.Mockito.times(2)).sendCustomMessage(captor.capture()); + List messages = captor.getAllValues(); + assertExpiredMessage(messages.get(0), "1001", "2001"); + assertExpiredMessage(messages.get(1), "2001", "1001"); + } + + @SuppressWarnings("unchecked") + private static void assertExpiredMessage(CustomC2cMsgBodyCmd message, String fromAccount, + String toAccount) { + assertEquals(fromAccount, message.getFromAccount()); + assertEquals(toAccount, message.getToAccount()); + assertEquals("CP_BUILD", message.getDesc()); + Map data = (Map) message.getData(); + assertEquals("RELATION_EXPIRED", data.get("noticeAction")); + assertEquals("RELATION_EXPIRED", data.get("action")); + assertEquals("EXPIRED", data.get("status")); + assertEquals("EXPIRED", data.get("result")); + assertEquals("EXPIRED", data.get("state")); + assertEquals(Boolean.TRUE, data.get("expired")); + assertEquals("SISTERS", data.get("relationType")); + assertTrue(data.get("content").toString().contains("\"relationType\":\"SISTERS\"")); + } + + private static UserProfile profile(Long userId) { + UserProfile userProfile = new UserProfile(); + userProfile.setId(userId); + userProfile.setAccount(userId.toString()); + userProfile.setUserNickname("user-" + userId); + userProfile.setCountryCode("SA"); + userProfile.setCountryName("Saudi Arabia"); + return userProfile; + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/CpApplyService.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/CpApplyService.java index 6c528fa2..0f894c84 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/CpApplyService.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/CpApplyService.java @@ -89,9 +89,19 @@ public interface CpApplyService extends BaseService { * 拒绝指定类型所有等待状态. */ void refuseStatusWait(Long acceptApplyUserId, String relationType); - - /** - * 获取我拒绝都信息. + + /** + * 拒绝两个用户之间除指定申请外的其他等待申请. + */ + List refuseOtherWaitBetweenUsers(Long excludeApplyId, Long userId, Long cpUserId); + + /** + * 获取两个用户之间已拒绝的申请. + */ + List listRefuseBetweenUsers(Long userId, Long cpUserId); + + /** + * 获取我拒绝都信息. */ List listRefuse(Long userId); diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/CpRelationshipService.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/CpRelationshipService.java index a32aecc9..788e1389 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/CpRelationshipService.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/CpRelationshipService.java @@ -36,9 +36,14 @@ public interface CpRelationshipService extends BaseService { */ boolean existsCp(Long userId, Long cpUserId, String relationType); - /** - * 获取分手中的cp信息集合 - */ + /** + * 两个用户之间是否存在任意正常关系. + */ + boolean existsRelationship(Long userId, Long cpUserId); + + /** + * 获取分手中的cp信息集合 + */ List getDismissingByUserId(Long userId); /** diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/impl/CpApplyServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/impl/CpApplyServiceImpl.java index a793fa43..ea5e1dbf 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/impl/CpApplyServiceImpl.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/impl/CpApplyServiceImpl.java @@ -115,19 +115,61 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp .eq(CpApply::getRelationType, CpRelationshipType.normalize(relationType)) .eq(CpApply::getAcceptApplyUserId, acceptApplyUserId) .execute(); - } - - @Override - public List listRefuse(Long userId) { - return query() - .eq(CpApply::getStatus, CpApplyStatus.REFUSE) + } + + @Override + public List refuseOtherWaitBetweenUsers(Long excludeApplyId, Long userId, Long cpUserId) { + List applyIds = query() + .select(CpApply::getId) + .eq(CpApply::getStatus, CpApplyStatus.WAIT) + .ne(Objects.nonNull(excludeApplyId), CpApply::getId, excludeApplyId) + .and(where -> where + .nested(w -> w.eq(CpApply::getSendApplyUserId, userId) + .eq(CpApply::getAcceptApplyUserId, cpUserId)) + .or() + .nested(w -> w.eq(CpApply::getSendApplyUserId, cpUserId) + .eq(CpApply::getAcceptApplyUserId, userId))) + .list() + .stream() + .map(CpApply::getId) + .filter(Objects::nonNull) + .toList(); + if (applyIds.isEmpty()) { + return applyIds; + } + update() + .set(CpApply::getStatus, CpApplyStatus.REFUSE) + .set(CpApply::getUpdateTime, LocalDateTime.now()) + .in(CpApply::getId, applyIds) + .execute(); + return applyIds; + } + + @Override + public List listRefuse(Long userId) { + return query() + .eq(CpApply::getStatus, CpApplyStatus.REFUSE) .eq(CpApply::getAcceptApplyUserId, userId) - .last(PageConstant.MAX_LIMIT) - .list(); - } - - @Override - public PageResult pageCpApply(CpApplyQryCmd query) { + .last(PageConstant.MAX_LIMIT) + .list(); + } + + @Override + public List listRefuseBetweenUsers(Long userId, Long cpUserId) { + return query() + .eq(CpApply::getStatus, CpApplyStatus.REFUSE) + .and(where -> where + .nested(w -> w.eq(CpApply::getSendApplyUserId, userId) + .eq(CpApply::getAcceptApplyUserId, cpUserId)) + .or() + .nested(w -> w.eq(CpApply::getSendApplyUserId, cpUserId) + .eq(CpApply::getAcceptApplyUserId, userId))) + .last(PageConstant.MAX_LIMIT) + .list(); + } + + @Override + public PageResult pageCpApply(CpApplyQryCmd query) { return query() .eq(Objects.nonNull(query.getSendApplyUserId()), CpApply::getSendApplyUserId, query.getSendApplyUserId()) diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/impl/CpRelationshipServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/impl/CpRelationshipServiceImpl.java index 3e92125d..6dc6d724 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/impl/CpRelationshipServiceImpl.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/user/user/impl/CpRelationshipServiceImpl.java @@ -66,10 +66,25 @@ public class CpRelationshipServiceImpl extends .nested(w -> w.eq(CpRelationship::getUserId, cpUserId).eq(CpRelationship::getCpUserId, userId)) ) .last(PageConstant.LIMIT_ONE) - .getOne() - ).map(cpRelationship -> Objects.nonNull(cpRelationship.getId())).orElse(Boolean.FALSE); - } - + .getOne() + ).map(cpRelationship -> Objects.nonNull(cpRelationship.getId())).orElse(Boolean.FALSE); + } + + @Override + public boolean existsRelationship(Long userId, Long cpUserId) { + return Optional.ofNullable( + query().select(CpRelationship::getId) + .eq(CpRelationship::getStatus, CpRelationshipStatus.NORMAL.name()) + .and(where -> where + .nested(w -> w.eq(CpRelationship::getUserId, userId).eq(CpRelationship::getCpUserId, cpUserId)) + .or() + .nested(w -> w.eq(CpRelationship::getUserId, cpUserId).eq(CpRelationship::getCpUserId, userId)) + ) + .last(PageConstant.LIMIT_ONE) + .getOne() + ).map(cpRelationship -> Objects.nonNull(cpRelationship.getId())).orElse(Boolean.FALSE); + } + @Override public List getDismissingByUserId(Long userId) { return getDismissingByUserId(userId, CpRelationshipType.CP.name());