From e5eeebf84ac7d21863a2dab5d904f30eda9cc929 Mon Sep 17 00:00:00 2001 From: hy001 Date: Mon, 25 May 2026 11:16:53 +0800 Subject: [PATCH 1/4] =?UTF-8?q?cp=E5=8A=9F=E8=83=BD=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/command/user/ProcessCpApplyCmd.java | 12 +++-- .../user/user/impl/CpApplyServiceImpl.java | 52 +++++++++---------- 2 files changed, 35 insertions(+), 29 deletions(-) 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 af7b5cab..fd0fc6b3 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 @@ -134,9 +134,15 @@ public class ProcessCpApplyCmd { private List autoRefuseWaitAppliesAfterAgree(CpApply cpApply, String relationType) { Map refusedApplyMap = new LinkedHashMap<>(); - addRefusedApplies(refusedApplyMap, - cpApplyService.refuseOtherWaitAppliesBetweenUsers(cpApply.getId(), - cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId())); + List samePairRefusedApplies = cpApplyService.refuseOtherWaitAppliesBetweenUsers( + cpApply.getId(), cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId()); + addRefusedApplies(refusedApplyMap, samePairRefusedApplies); + if (!samePairRefusedApplies.isEmpty()) { + log.info("[CP] auto refused other wait applies between same users after agree, " + + "agreeApplyId={}, sendUserId={}, acceptUserId={}, applyIds={}", + cpApply.getId(), cpApply.getSendApplyUserId(), cpApply.getAcceptApplyUserId(), + samePairRefusedApplies.stream().map(CpApply::getId).toList()); + } addRefusedApplies(refusedApplyMap, refuseWaitAppliesIfLimitReached(cpApply, cpApply.getSendApplyUserId(), relationType)); addRefusedApplies(refusedApplyMap, 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 e7a88513..eed281c6 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 @@ -37,14 +37,14 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp @Override public void dismiss(Long requestUserId, String relationType) { update() - .set(CpApply::getStatus, CpApplyStatus.DISMISS) + .set(CpApply::getStatus, CpApplyStatus.DISMISS.name()) .set(CpApply::getUpdateTime, LocalDateTime.now()) .set(CpApply::getUpdateUser, requestUserId) .eq(CpApply::getRelationType, CpRelationshipType.normalize(relationType)) - .eq(CpApply::getStatus, CpApplyStatus.AGREE) + .eq(CpApply::getStatus, CpApplyStatus.AGREE.name()) .and(where -> where.eq(CpApply::getSendApplyUserId, requestUserId) - .or().eq(CpApply::getAcceptApplyUserId, requestUserId)) - .last(PageConstant.LIMIT_ONE) + .or().eq(CpApply::getAcceptApplyUserId, requestUserId)) + .last(PageConstant.LIMIT_ONE) .execute(); } @@ -58,7 +58,7 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp return Optional.ofNullable( query() .select(CpApply::getId) - .eq(CpApply::getStatus, CpApplyStatus.WAIT) + .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) .eq(CpApply::getRelationType, CpRelationshipType.normalize(relationType)) .eq(CpApply::getAcceptApplyUserId, acceptApplyUserId) .eq(CpApply::getSendApplyUserId, sendApplyUserId) @@ -88,19 +88,19 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp .select(CpApply::getId) .eq(CpApply::getSendApplyUserId, sendApplyUserId) .eq(CpApply::getRelationType, CpRelationshipType.normalize(relationType)) - .eq(CpApply::getStatus, CpApplyStatus.WAIT) + .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) .getOne()) - .map(cpApply -> Objects.nonNull(cpApply.getId())) - .orElse(Boolean.FALSE); - } - - @Override - public boolean changeStatusById(Long id, CpApplyStatus status) { - return update() - .set(CpApply::getStatus, status) - .eq(CpApply::getId, id) - .execute(); - } + .map(cpApply -> Objects.nonNull(cpApply.getId())) + .orElse(Boolean.FALSE); + } + + @Override + public boolean changeStatusById(Long id, CpApplyStatus status) { + return update() + .set(CpApply::getStatus, status.name()) + .eq(CpApply::getId, id) + .execute(); + } @Override public void refuseStatusWait(Long acceptApplyUserId) { @@ -110,8 +110,8 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp @Override public void refuseStatusWait(Long acceptApplyUserId, String relationType) { update() - .set(CpApply::getStatus, CpApplyStatus.REFUSE) - .eq(CpApply::getStatus, CpApplyStatus.WAIT) + .set(CpApply::getStatus, CpApplyStatus.REFUSE.name()) + .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) .eq(CpApply::getRelationType, CpRelationshipType.normalize(relationType)) .eq(CpApply::getAcceptApplyUserId, acceptApplyUserId) .execute(); @@ -130,7 +130,7 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp public List refuseOtherWaitAppliesBetweenUsers(Long excludeApplyId, Long userId, Long cpUserId) { List applies = query() - .eq(CpApply::getStatus, CpApplyStatus.WAIT) + .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) .ne(Objects.nonNull(excludeApplyId), CpApply::getId, excludeApplyId) .and(where -> where .nested(w -> w.eq(CpApply::getSendApplyUserId, userId) @@ -147,7 +147,7 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp return applies; } update() - .set(CpApply::getStatus, CpApplyStatus.REFUSE) + .set(CpApply::getStatus, CpApplyStatus.REFUSE.name()) .set(CpApply::getUpdateTime, LocalDateTime.now()) .in(CpApply::getId, applyIds) .execute(); @@ -159,7 +159,7 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp public List refuseWaitAppliesByUserAndRelation(Long excludeApplyId, Long userId, String relationType) { List applies = query() - .eq(CpApply::getStatus, CpApplyStatus.WAIT) + .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) .eq(CpApply::getRelationType, CpRelationshipType.normalize(relationType)) .ne(Objects.nonNull(excludeApplyId), CpApply::getId, excludeApplyId) .and(where -> where.eq(CpApply::getSendApplyUserId, userId) @@ -174,7 +174,7 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp return applies; } update() - .set(CpApply::getStatus, CpApplyStatus.REFUSE) + .set(CpApply::getStatus, CpApplyStatus.REFUSE.name()) .set(CpApply::getUpdateTime, LocalDateTime.now()) .in(CpApply::getId, applyIds) .execute(); @@ -185,8 +185,8 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp @Override public List listRefuse(Long userId) { return query() - .eq(CpApply::getStatus, CpApplyStatus.REFUSE) - .eq(CpApply::getAcceptApplyUserId, userId) + .eq(CpApply::getStatus, CpApplyStatus.REFUSE.name()) + .eq(CpApply::getAcceptApplyUserId, userId) .last(PageConstant.MAX_LIMIT) .list(); } @@ -194,7 +194,7 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp @Override public List listRefuseBetweenUsers(Long userId, Long cpUserId) { return query() - .eq(CpApply::getStatus, CpApplyStatus.REFUSE) + .eq(CpApply::getStatus, CpApplyStatus.REFUSE.name()) .and(where -> where .nested(w -> w.eq(CpApply::getSendApplyUserId, userId) .eq(CpApply::getAcceptApplyUserId, cpUserId)) From 0260498bd371efd8176484e0443a4fa5b1c34598 Mon Sep 17 00:00:00 2001 From: hy001 Date: Mon, 25 May 2026 14:32:15 +0800 Subject: [PATCH 2/4] =?UTF-8?q?CP=E5=85=B3=E7=B3=BB=E7=94=B3=E8=AF=B7=20?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/command/user/SendCpApplyCmdExe.java | 3 + .../command/user/query/CpApplyIdQryExe.java | 29 +++-- .../user/query/CpApplyStatusQryExe.java | 20 +++- .../common/cp/CpRelationC2cNoticeUtils.java | 9 ++ .../app/listener/user/CpApplyListener.java | 5 +- .../user/query/CpApplyIdQryExeTest.java | 43 ++++++++ .../user/query/CpApplyStatusQryExeTest.java | 18 +++ .../listener/user/CpApplyListenerTest.java | 35 +++++- .../rds/service/user/user/CpApplyService.java | 12 +- .../user/user/impl/CpApplyServiceImpl.java | 56 ++++++++-- ...mManualSalaryPaymentClientServiceImpl.java | 19 ++-- ...ualSalaryPaymentClientServiceImplTest.java | 103 ++++++++++++++++++ 12 files changed, 321 insertions(+), 31 deletions(-) create mode 100644 rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyIdQryExeTest.java create mode 100644 rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java 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 18cb6dc9..9a022bea 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 @@ -108,6 +108,9 @@ public class SendCpApplyCmdExe { ResponseAssert.isFalse(UserRelationErrorCode.UNAVAILABLE_OPS_YOURSELF, Objects.equals(sendUserId, acceptUserId)); + cpApplyService.expireOverdueWaitApplies(sendUserId); + cpApplyService.expireOverdueWaitApplies(acceptUserId); + boolean eqRegion = userRegionGateway.checkEqRegion(sendUserId, acceptUserId); ResponseAssert.isTrue(UserRelationErrorCode.UNAVAILABLE_NOT_REGION, eqRegion); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyIdQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyIdQryExe.java index a0e19a98..bf20ba0a 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyIdQryExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyIdQryExe.java @@ -3,7 +3,8 @@ package com.red.circle.other.app.command.user.query; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyIdQueryCmd; import com.red.circle.other.infra.database.rds.service.user.user.CpApplyService; import com.red.circle.other.infra.enums.user.user.CpRelationshipType; -import java.util.Optional; +import java.util.Objects; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -16,13 +17,27 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class CpApplyIdQryExe { - private final CpApplyService cpApplyService; - + private final CpApplyService cpApplyService; + public String execute(CpApplyIdQueryCmd cmd) { - return Optional.ofNullable(cpApplyService.getId(cmd.requiredReqUserId(), cmd.getUserId(), - CpRelationshipType.normalize(cmd.getRelationType()))) + Long currentUserId = cmd.requiredReqUserId(); + Long targetUserId = cmd.getUserId(); + String relationType = CpRelationshipType.normalize(cmd.getRelationType()); + cpApplyService.expireOverdueWaitApplies(currentUserId); + cpApplyService.expireOverdueWaitApplies(targetUserId); + + Long applyId = cpApplyService.getId(targetUserId, currentUserId, relationType); + if (isEmptyApplyId(applyId)) { + applyId = cpApplyService.getId(currentUserId, targetUserId, relationType); + } + return Optional.ofNullable(applyId) + .filter(id -> id > 0) .map(String::valueOf) .orElse(null); } - -} + + private boolean isEmptyApplyId(Long applyId) { + return Objects.isNull(applyId) || applyId <= 0; + } + +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExe.java index 6be69d5e..3eea2de8 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExe.java @@ -3,6 +3,8 @@ package com.red.circle.other.app.command.user.query; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyStatusQueryCmd; 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 java.time.LocalDateTime; import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -14,10 +16,26 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class CpApplyStatusQryExe { + private static final long APPLY_WAIT_EXPIRE_HOURS = 24L; + private final CpApplyService cpApplyService; public String execute(CpApplyStatusQueryCmd cmd) { CpApply cpApply = cpApplyService.getById(cmd.getApplyId()); - return Objects.nonNull(cpApply) ? cpApply.getStatus() : null; + if (Objects.isNull(cpApply)) { + return null; + } + if (isOverdueWait(cpApply)) { + cpApplyService.expireWaitById(cpApply.getId()); + return CpApplyStatus.EXPIRE.name(); + } + return cpApply.getStatus(); + } + + private boolean isOverdueWait(CpApply cpApply) { + return Objects.equals(cpApply.getStatus(), CpApplyStatus.WAIT.name()) + && Objects.nonNull(cpApply.getCreateTime()) + && cpApply.getCreateTime().toLocalDateTime() + .isBefore(LocalDateTime.now().minusHours(APPLY_WAIT_EXPIRE_HOURS)); } } 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 index 75ec60e7..0f95321a 100644 --- 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 @@ -9,6 +9,7 @@ import com.red.circle.tool.core.json.JacksonUtils; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; public final class CpRelationC2cNoticeUtils { @@ -36,9 +37,17 @@ public final class CpRelationC2cNoticeUtils { content.put("applyType", isReconcile ? "RECONCILE" : "APPLY"); content.put("relationType", relationType.name()); putAcceptProfile(content, acceptProfile); + long createTimeSeconds = System.currentTimeMillis() / 1000; + long expireTimeSeconds = createTimeSeconds + TimeUnit.DAYS.toSeconds(1); + content.put("createTime", createTimeSeconds); + content.put("sendTime", createTimeSeconds); + content.put("dismissEndTime", expireTimeSeconds); Map payload = basePayload(applyId, relationType.name(), title, ACTION_INVITE_PENDING, "PENDING", content); + payload.put("createTime", createTimeSeconds); + payload.put("sendTime", createTimeSeconds); + payload.put("dismissEndTime", expireTimeSeconds); putSenderProfile(payload, senderProfile); putAcceptProfile(payload, acceptProfile); return buildMessage(fromUserId, toUserId, payload); 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 eba27308..f9478b13 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 @@ -56,12 +56,15 @@ public class CpApplyListener implements MessageListener { // 等待超时 if (Objects.equals(cpApply.getStatus(), CpApplyStatus.WAIT.name())) { expire(cpApply); + } else { + log.info("[CP] skip expire cp apply, applyId={}, status={}", cpApply.getId(), + cpApply.getStatus()); } }); } private void expire(CpApply cpApply) { - boolean changed = cpApplyService.changeStatusById(cpApply.getId(), CpApplyStatus.EXPIRE); + boolean changed = cpApplyService.expireWaitById(cpApply.getId()); if (changed) { sendExpiredNotice(cpApply); } diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyIdQryExeTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyIdQryExeTest.java new file mode 100644 index 00000000..f1a41416 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyIdQryExeTest.java @@ -0,0 +1,43 @@ +package com.red.circle.other.app.command.user.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyIdQueryCmd; +import com.red.circle.other.infra.database.rds.service.user.user.CpApplyService; +import org.junit.jupiter.api.Test; + +class CpApplyIdQryExeTest { + + @Test + void shouldReturnApplyIdSentByCurrentUser() { + CpApplyService cpApplyService = mock(CpApplyService.class); + when(cpApplyService.getId(2001L, 1001L, "BROTHER")).thenReturn(3001L); + + CpApplyIdQryExe exe = new CpApplyIdQryExe(cpApplyService); + + assertEquals("3001", exe.execute(cmd(1001L, 2001L, "BROTHER"))); + verify(cpApplyService).expireOverdueWaitApplies(1001L); + verify(cpApplyService).expireOverdueWaitApplies(2001L); + } + + @Test + void shouldFallbackToApplyIdReceivedByCurrentUser() { + CpApplyService cpApplyService = mock(CpApplyService.class); + when(cpApplyService.getId(1001L, 2001L, "CP")).thenReturn(3002L); + + CpApplyIdQryExe exe = new CpApplyIdQryExe(cpApplyService); + + assertEquals("3002", exe.execute(cmd(1001L, 2001L, "CP"))); + } + + private static CpApplyIdQueryCmd cmd(Long reqUserId, Long targetUserId, String relationType) { + CpApplyIdQueryCmd cmd = new CpApplyIdQueryCmd(); + cmd.setReqUserId(reqUserId); + cmd.setUserId(targetUserId); + cmd.setRelationType(relationType); + return cmd; + } +} diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExeTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExeTest.java index 34b27365..11ea8ada 100644 --- a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExeTest.java +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExeTest.java @@ -3,11 +3,14 @@ package com.red.circle.other.app.command.user.query; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyStatusQueryCmd; 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 java.sql.Timestamp; +import java.time.LocalDateTime; import org.junit.jupiter.api.Test; class CpApplyStatusQryExeTest { @@ -30,6 +33,21 @@ class CpApplyStatusQryExeTest { assertNull(exe.execute(cmd(3001L))); } + @Test + void shouldExpireOverdueWaitApplyWhenQueryingStatus() { + CpApplyService cpApplyService = mock(CpApplyService.class); + CpApply cpApply = new CpApply() + .setId(3001L) + .setStatus("WAIT"); + cpApply.setCreateTime(Timestamp.valueOf(LocalDateTime.now().minusHours(25))); + when(cpApplyService.getById(3001L)).thenReturn(cpApply); + + CpApplyStatusQryExe exe = new CpApplyStatusQryExe(cpApplyService); + + assertEquals("EXPIRE", exe.execute(cmd(3001L))); + verify(cpApplyService).expireWaitById(3001L); + } + private static CpApplyStatusQueryCmd cmd(Long applyId) { CpApplyStatusQueryCmd cmd = new CpApplyStatusQueryCmd(); cmd.setApplyId(applyId); 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 index 0bda6de2..52f68eca 100644 --- 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 @@ -6,6 +6,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; 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.common.business.core.enums.SysOriginPlatformEnum; @@ -45,7 +47,7 @@ class CpApplyListenerTest { .setStatus(CpApplyStatus.WAIT.name()); when(cpApplyService.getById(3001L)).thenReturn(apply); - when(cpApplyService.changeStatusById(3001L, CpApplyStatus.EXPIRE)).thenReturn(true); + when(cpApplyService.expireWaitById(3001L)).thenReturn(true); when(userProfileGateway.getByUserId(1001L)).thenReturn(profile(1001L)); when(userProfileGateway.getByUserId(2001L)).thenReturn(profile(2001L)); when(imMessageClient.sendCustomMessage(any(CustomC2cMsgBodyCmd.class))) @@ -68,12 +70,41 @@ class CpApplyListenerTest { ArgumentCaptor captor = ArgumentCaptor.forClass( CustomC2cMsgBodyCmd.class); InOrder inOrder = inOrder(cpApplyService, imMessageClient); - inOrder.verify(cpApplyService).changeStatusById(3001L, CpApplyStatus.EXPIRE); + inOrder.verify(cpApplyService).expireWaitById(3001L); inOrder.verify(imMessageClient).sendCustomMessage(captor.capture()); List messages = captor.getAllValues(); assertExpiredMessage(messages.get(0), "1001", "2001"); } + @Test + void nonWaitApplyShouldNotExpireAgain() { + CpApplyService cpApplyService = mock(CpApplyService.class); + ImMessageClient imMessageClient = mock(ImMessageClient.class); + UserProfileGateway userProfileGateway = mock(UserProfileGateway.class); + MessageEventProcess messageEventProcess = mock(MessageEventProcess.class); + CpApply apply = new CpApply() + .setId(3001L) + .setStatus(CpApplyStatus.EXPIRE.name()); + + when(cpApplyService.getById(3001L)).thenReturn(apply); + 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, messageEventProcess); + + assertEquals(Action.SUCCESS, listener.consume(new ConsumerMessage())); + verify(cpApplyService, never()).expireWaitById(3001L); + verify(imMessageClient, never()).sendCustomMessage(any(CustomC2cMsgBodyCmd.class)); + } + @SuppressWarnings("unchecked") private static void assertExpiredMessage(CustomC2cMsgBodyCmd message, String fromAccount, String toAccount) { 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 332648cc..bfdfd331 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 @@ -76,7 +76,17 @@ public interface CpApplyService extends BaseService { * @param status 状态 * @return true 成功,false 失败 */ - boolean changeStatusById(Long id, CpApplyStatus status); + boolean changeStatusById(Long id, CpApplyStatus status); + + /** + * 只把等待中的申请置为超时. + */ + boolean expireWaitById(Long id); + + /** + * 清理用户相关超过 24 小时仍处于等待状态的申请. + */ + void expireOverdueWaitApplies(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 eed281c6..9a9aadae 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 @@ -10,9 +10,10 @@ 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.other.infra.enums.user.user.CpRelationshipType; -import com.red.circle.other.inner.model.cmd.user.CpApplyQryCmd; -import java.time.LocalDateTime; -import java.util.List; +import com.red.circle.other.inner.model.cmd.user.CpApplyQryCmd; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; import java.util.Objects; import java.util.Optional; import org.springframework.stereotype.Service; @@ -26,9 +27,11 @@ import org.springframework.stereotype.Service; * @since 2021-07-13 */ @Service -public class CpApplyServiceImpl extends BaseServiceImpl implements - CpApplyService { - +public class CpApplyServiceImpl extends BaseServiceImpl implements + CpApplyService { + + private static final long APPLY_WAIT_EXPIRE_HOURS = 24L; + @Override public void dismiss(Long requestUserId) { dismiss(requestUserId, CpRelationshipType.CP.name()); @@ -62,10 +65,12 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp .eq(CpApply::getRelationType, CpRelationshipType.normalize(relationType)) .eq(CpApply::getAcceptApplyUserId, acceptApplyUserId) .eq(CpApply::getSendApplyUserId, sendApplyUserId) - .last(PageConstant.LIMIT_ONE) - .getOne() - ).map(CpApply::getId).orElse(null); - } + .gt(CpApply::getCreateTime, waitExpireBefore()) + .orderByDesc(CpApply::getCreateTime) + .last(PageConstant.LIMIT_ONE) + .getOne() + ).map(CpApply::getId).orElse(null); + } @Override public boolean exists(Long acceptApplyUserId, Long sendApplyUserId) { @@ -89,6 +94,7 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp .eq(CpApply::getSendApplyUserId, sendApplyUserId) .eq(CpApply::getRelationType, CpRelationshipType.normalize(relationType)) .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) + .gt(CpApply::getCreateTime, waitExpireBefore()) .getOne()) .map(cpApply -> Objects.nonNull(cpApply.getId())) .orElse(Boolean.FALSE); @@ -98,9 +104,39 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp public boolean changeStatusById(Long id, CpApplyStatus status) { return update() .set(CpApply::getStatus, status.name()) + .set(CpApply::getUpdateTime, LocalDateTime.now()) .eq(CpApply::getId, id) .execute(); } + + @Override + public boolean expireWaitById(Long id) { + return update() + .set(CpApply::getStatus, CpApplyStatus.EXPIRE.name()) + .set(CpApply::getUpdateTime, LocalDateTime.now()) + .eq(CpApply::getId, id) + .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) + .execute(); + } + + @Override + public void expireOverdueWaitApplies(Long userId) { + if (Objects.isNull(userId)) { + return; + } + update() + .set(CpApply::getStatus, CpApplyStatus.EXPIRE.name()) + .set(CpApply::getUpdateTime, LocalDateTime.now()) + .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) + .lt(CpApply::getCreateTime, waitExpireBefore()) + .and(where -> where.eq(CpApply::getSendApplyUserId, userId) + .or().eq(CpApply::getAcceptApplyUserId, userId)) + .execute(); + } + + private Timestamp waitExpireBefore() { + return Timestamp.valueOf(LocalDateTime.now().minusHours(APPLY_WAIT_EXPIRE_HOURS)); + } @Override public void refuseStatusWait(Long acceptApplyUserId) { diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImpl.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImpl.java index d9cdcace..f756861d 100644 --- a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImpl.java +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImpl.java @@ -134,7 +134,8 @@ public class TeamManualSalaryPaymentClientServiceImpl implements billBelong, row.getSysOrigin()); for (PolicyPayment policyPayment : row.getPayments()) { - if (Boolean.TRUE.equals(teamSalaryPaymentService.checkRepeatPayment(payment.getId(), + if (policyPayment.getSalary().compareTo(BigDecimal.ZERO) <= 0 + && Boolean.TRUE.equals(teamSalaryPaymentService.checkRepeatPayment(payment.getId(), row.getUserId(), row.getRegion(), policyPayment.getLevel(), policyPayment.getAnchor()))) { continue; @@ -501,32 +502,32 @@ public class TeamManualSalaryPaymentClientServiceImpl implements .setScale(2, RoundingMode.DOWN); BigDecimal agentMissing = agentExpected.subtract(paymentSummary.getAgentIssuedSalary()) .setScale(2, RoundingMode.DOWN); - boolean memberSettled = isRoleSettled(cyclePayments, userId, region, billTarget.getLevel(), - Boolean.TRUE, memberExpected); - boolean agentSettled = isRoleSettled(cyclePayments, userId, region, billTarget.getLevel(), - Boolean.FALSE, agentExpected); + boolean memberHasSettlementRecord = hasPaidPolicy(cyclePayments, userId, region, + billTarget.getLevel(), Boolean.TRUE); + boolean agentHasSettlementRecord = hasPaidPolicy(cyclePayments, userId, region, + billTarget.getLevel(), Boolean.FALSE); List payments = new ArrayList<>(); if (remainingPayable.compareTo(BigDecimal.ZERO) <= 0) { - if (!memberSettled && memberExpected.compareTo(BigDecimal.ZERO) > 0) { + if (!memberHasSettlementRecord && memberExpected.compareTo(BigDecimal.ZERO) > 0) { payments.add(new PolicyPayment(policy, billTarget.getLevel(), BigDecimal.ZERO.setScale(2, RoundingMode.DOWN), Boolean.TRUE)); } - if (!agentSettled && agentExpected.compareTo(BigDecimal.ZERO) > 0) { + if (!agentHasSettlementRecord && agentExpected.compareTo(BigDecimal.ZERO) > 0) { payments.add(new PolicyPayment(policy, billTarget.getLevel(), BigDecimal.ZERO.setScale(2, RoundingMode.DOWN), Boolean.FALSE)); } return payments; } - if (!memberSettled && memberMissing.compareTo(BigDecimal.ZERO) > 0) { + if (memberMissing.compareTo(BigDecimal.ZERO) > 0) { BigDecimal issueSalary = memberMissing.min(remainingPayable).setScale(2, RoundingMode.DOWN); if (issueSalary.compareTo(BigDecimal.ZERO) > 0) { payments.add(new PolicyPayment(policy, billTarget.getLevel(), issueSalary, Boolean.TRUE)); remainingPayable = remainingPayable.subtract(issueSalary).setScale(2, RoundingMode.DOWN); } } - if (!agentSettled && agentMissing.compareTo(BigDecimal.ZERO) > 0 + if (agentMissing.compareTo(BigDecimal.ZERO) > 0 && remainingPayable.compareTo(BigDecimal.ZERO) > 0) { BigDecimal issueSalary = agentMissing.min(remainingPayable).setScale(2, RoundingMode.DOWN); if (issueSalary.compareTo(BigDecimal.ZERO) > 0) { diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java b/rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java new file mode 100644 index 00000000..9656bf8b --- /dev/null +++ b/rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java @@ -0,0 +1,103 @@ +package com.red.circle.other.app.inner.service.team.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.red.circle.other.infra.database.mongo.dto.team.TeamBillMemberTarget; +import com.red.circle.other.infra.database.mongo.dto.team.TeamSalaryPaymentDetails; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamSalaryPayment; +import java.lang.reflect.Constructor; +import java.math.BigDecimal; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +class TeamManualSalaryPaymentClientServiceImplTest { + + @Test + void buildSettlementPaymentsShouldTopUpWhenCurrentLevelRecordExistsButSalaryIsInsufficient() + throws Exception { + TeamManualSalaryPaymentClientServiceImpl service = + new TeamManualSalaryPaymentClientServiceImpl( + null, null, null, null, null, null, null, null); + Long userId = 2054591808909930498L; + String region = "2046067717605224449"; + + TeamBillMemberTarget billTarget = new TeamBillMemberTarget() + .setLevel(6) + .setMemberSalary(new BigDecimal("35.00")) + .setOwnSalary(new BigDecimal("8.75")); + TeamSalaryPayment currentPayment = new TeamSalaryPayment() + .setSalarySendDetails(List.of(new TeamSalaryPaymentDetails() + .setTeamMemberId(userId) + .setTeamRegionId(region) + .setPolicyLevel(6) + .setAnchor(Boolean.TRUE) + .setSalary(new BigDecimal("3.00")) + .setRecycled(Boolean.FALSE))); + + Object paymentSummary = paymentSummary( + new BigDecimal("31.50"), + new BigDecimal("22.00"), + new BigDecimal("9.50")); + + List payments = ReflectionTestUtils.invokeMethod(service, "buildSettlementPayments", + null, billTarget, List.of(currentPayment), userId, region, paymentSummary, + new BigDecimal("12.25"), false); + + assertEquals(1, payments.size()); + Object payment = payments.get(0); + assertEquals(6, ReflectionTestUtils.getField(payment, "level")); + assertEquals(Boolean.TRUE, ReflectionTestUtils.getField(payment, "anchor")); + assertEquals(new BigDecimal("12.25"), ReflectionTestUtils.getField(payment, "salary")); + } + + @Test + void buildSettlementPaymentsShouldSettleZeroWhenPayableSalaryIsNegative() throws Exception { + TeamManualSalaryPaymentClientServiceImpl service = + new TeamManualSalaryPaymentClientServiceImpl( + null, null, null, null, null, null, null, null); + Long userId = 1008071L; + String region = "2046067717605224449"; + + TeamBillMemberTarget billTarget = new TeamBillMemberTarget() + .setLevel(6) + .setMemberSalary(new BigDecimal("35.00")) + .setOwnSalary(new BigDecimal("8.75")); + Object paymentSummary = paymentSummary( + new BigDecimal("45.00"), + new BigDecimal("36.00"), + new BigDecimal("9.00")); + + List payments = ReflectionTestUtils.invokeMethod(service, "buildSettlementPayments", + null, billTarget, List.of(), userId, region, paymentSummary, + new BigDecimal("-1.25"), false); + + assertEquals(2, payments.size()); + assertEquals(BigDecimal.ZERO.setScale(2), ReflectionTestUtils.getField(payments.get(0), + "salary")); + assertEquals(BigDecimal.ZERO.setScale(2), ReflectionTestUtils.getField(payments.get(1), + "salary")); + } + + private Object paymentSummary(BigDecimal totalIssuedSalary, BigDecimal memberIssuedSalary, + BigDecimal agentIssuedSalary) throws Exception { + Class summaryClass = Class.forName( + "com.red.circle.other.app.inner.service.team.impl." + + "TeamManualSalaryPaymentClientServiceImpl$PaymentSummary"); + Constructor constructor = summaryClass.getDeclaredConstructor( + BigDecimal.class, + BigDecimal.class, + BigDecimal.class, + BigDecimal.class, + BigDecimal.class, + BigDecimal.class); + constructor.setAccessible(true); + return constructor.newInstance( + totalIssuedSalary, + memberIssuedSalary, + agentIssuedSalary, + BigDecimal.ZERO.setScale(2), + BigDecimal.ZERO.setScale(2), + BigDecimal.ZERO.setScale(2)); + } +} From c2e28b5cb3601f14559a7f51d5a8b33d6fba9d81 Mon Sep 17 00:00:00 2001 From: hy001 Date: Mon, 25 May 2026 16:22:18 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CpRelationshipRestController.java | 17 ++- .../app/command/user/SendCpApplyCmdExe.java | 5 +- .../user/query/CpGiftRelationCheckQryExe.java | 81 +++++++++++ .../relation/UserCpRelationServiceImpl.java | 17 ++- .../query/CpGiftRelationCheckQryExeTest.java | 119 +++++++++++++++ .../relation/cp/CpGiftRelationCheckCO.java | 69 +++++++++ .../relation/cp/CpGiftRelationCheckCmd.java | 29 ++++ .../user/relation/UserCpRelationService.java | 6 +- .../enums/user/user/CpRelationshipType.java | 7 + ...ualSalaryPaymentClientServiceImplTest.java | 136 ++++++++++++++++++ 10 files changed, 476 insertions(+), 10 deletions(-) create mode 100644 rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpGiftRelationCheckQryExe.java create mode 100644 rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpGiftRelationCheckQryExeTest.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/user/relation/cp/CpGiftRelationCheckCO.java create mode 100644 rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/user/relation/cp/CpGiftRelationCheckCmd.java diff --git a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/user/relation/CpRelationshipRestController.java b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/user/relation/CpRelationshipRestController.java index 88b29ea1..c66eec56 100644 --- a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/user/relation/CpRelationshipRestController.java +++ b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/user/relation/CpRelationshipRestController.java @@ -4,6 +4,7 @@ import com.red.circle.common.business.dto.cmd.AppExtCommand; import com.red.circle.framework.web.controller.BaseController; import com.red.circle.other.app.command.user.LoveLetterQueryCmdExe; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpDismissApplyCO; +import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpGiftRelationCheckCO; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpPairUserProfileCO; import com.red.circle.other.domain.model.cp.loveletter.LoveLetterDetailCO; import com.red.circle.other.domain.model.cp.loveletter.LoveLetterWallCO; @@ -11,6 +12,7 @@ import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyIdQueryCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyDismissCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyStatusQueryCmd; +import com.red.circle.other.app.dto.cmd.user.relation.cp.CpGiftRelationCheckCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpRelationshipQueryCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.ProcessApplyCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.loveletter.SendLoveLetterCmd; @@ -124,7 +126,20 @@ public class CpRelationshipRestController extends BaseController { } /** - * 发送CP告白信封. + * CP礼物关系名额校验. + * + * @eo.name CP礼物关系名额校验. + * @eo.url /gift-relation/check + * @eo.method get + * @eo.request-type formdata + */ + @GetMapping("/gift-relation/check") + public CpGiftRelationCheckCO checkGiftRelation(@Validated CpGiftRelationCheckCmd cmd) { + return userCpRelationService.checkGiftRelation(cmd); + } + + /** + * 发送CP告白信封. * * @eo.name 发送CP告白信封 * @eo.url /send-love-letter 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 9a022bea..9a9eb9b7 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 @@ -42,9 +42,6 @@ import org.springframework.stereotype.Component; @Slf4j public class SendCpApplyCmdExe { - private static final int CP_MAX_COUNT = 1; - private static final int FRIEND_MAX_COUNT = 3; - private final CpApplyService cpApplyService; private final ImMessageClient imMessageClient; private final WalletGoldClient walletGoldClient; @@ -207,7 +204,7 @@ public class SendCpApplyCmdExe { } private int relationMaxCount(CpRelationshipType relationType) { - return Objects.equals(relationType, CpRelationshipType.CP) ? CP_MAX_COUNT : FRIEND_MAX_COUNT; + return relationType.maxCount(); } private CpRelationshipType parseRelationType(String relationTypeValue) { diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpGiftRelationCheckQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpGiftRelationCheckQryExe.java new file mode 100644 index 00000000..e9e42b6e --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpGiftRelationCheckQryExe.java @@ -0,0 +1,81 @@ +package com.red.circle.other.app.command.user.query; + +import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpGiftRelationCheckCO; +import com.red.circle.other.app.dto.cmd.user.relation.cp.CpGiftRelationCheckCmd; +import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipService; +import com.red.circle.other.infra.enums.user.user.CpRelationshipType; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * Check whether a relationship gift can trigger a relationship apply. + */ +@Component +@RequiredArgsConstructor +public class CpGiftRelationCheckQryExe { + + private static final String OK_MSG = "OK"; + private static final String USER_FULL_MSG = "Your relationships are full."; + private static final String ACCEPT_USER_FULL_MSG = "The other party's relationships are full."; + private static final String RELATION_TYPE_NOT_SUPPORTED_MSG = "relationType is not supported."; + + private final CpRelationshipService cpRelationshipService; + + public CpGiftRelationCheckCO execute(CpGiftRelationCheckCmd cmd) { + Long userId = cmd.requiredReqUserId(); + Long acceptUserId = cmd.getAcceptUserId(); + CpRelationshipType relationType = parseRelationType(cmd.getRelationType()); + if (Objects.isNull(relationType)) { + return new CpGiftRelationCheckCO() + .setStatus(false) + .setMsg(RELATION_TYPE_NOT_SUPPORTED_MSG) + .setRelationType(cmd.getRelationType()) + .setUserId(userId) + .setAcceptUserId(acceptUserId); + } + int maxCount = relationType.maxCount(); + int userCount = cpRelationshipService.countCp(userId, relationType.name()); + int acceptUserCount = cpRelationshipService.countCp(acceptUserId, relationType.name()); + boolean userFull = userCount >= maxCount; + boolean acceptUserFull = acceptUserCount >= maxCount; + boolean skipApplyCheck = Objects.equals(userId, acceptUserId) + || cpRelationshipService.existsRelationship(userId, acceptUserId) + || Objects.nonNull(cpRelationshipService.getDismissingCp(userId, acceptUserId, + relationType.name())); + boolean canContinue = skipApplyCheck || (!userFull && !acceptUserFull); + + return new CpGiftRelationCheckCO() + .setStatus(canContinue) + .setMsg(message(canContinue, userFull, acceptUserFull)) + .setRelationType(relationType.name()) + .setUserId(userId) + .setAcceptUserId(acceptUserId) + .setUserRelationCount(userCount) + .setAcceptUserRelationCount(acceptUserCount) + .setMaxRelationCount(maxCount) + .setUserRelationFull(userFull) + .setAcceptUserRelationFull(acceptUserFull); + } + + private String message(boolean canContinue, boolean userFull, boolean acceptUserFull) { + if (canContinue) { + return OK_MSG; + } + if (userFull) { + return USER_FULL_MSG; + } + if (acceptUserFull) { + return ACCEPT_USER_FULL_MSG; + } + return OK_MSG; + } + + private CpRelationshipType parseRelationType(String relationTypeValue) { + try { + return CpRelationshipType.of(relationTypeValue); + } catch (IllegalArgumentException ex) { + return null; + } + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/relation/UserCpRelationServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/relation/UserCpRelationServiceImpl.java index 4c1994f8..353aec0c 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/relation/UserCpRelationServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/relation/UserCpRelationServiceImpl.java @@ -9,17 +9,20 @@ import com.red.circle.other.app.command.user.SendCpApplyCmdExe; import com.red.circle.other.app.command.user.SendLoveLetterCmdExe; import com.red.circle.other.app.command.user.query.CpApplyIdQryExe; import com.red.circle.other.app.command.user.query.CpApplyStatusQryExe; +import com.red.circle.other.app.command.user.query.CpGiftRelationCheckQryExe; import com.red.circle.other.app.command.user.query.UserCpPairUserProfileQryExe; -import com.red.circle.other.app.command.user.query.UserCpRelationQryExe; -import com.red.circle.other.app.command.user.query.UserCpRelationQueryV4Exe; +import com.red.circle.other.app.command.user.query.UserCpRelationQryExe; +import com.red.circle.other.app.command.user.query.UserCpRelationQueryV4Exe; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpCabinUserProfileCO; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpDismissApplyCO; +import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpGiftRelationCheckCO; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpPairUserProfileCO; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpUserProfileCO; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyIdQueryCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyStatusQueryCmd; +import com.red.circle.other.app.dto.cmd.user.relation.cp.CpGiftRelationCheckCmd; import com.red.circle.other.domain.model.cp.loveletter.LoveLetterDetailCO; -import com.red.circle.other.domain.model.cp.loveletter.LoveLetterWallCO; +import com.red.circle.other.domain.model.cp.loveletter.LoveLetterWallCO; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyDismissCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpRelationshipQueryCmd; @@ -43,6 +46,7 @@ public class UserCpRelationServiceImpl implements UserCpRelationService { private final CpApplyIdQryExe cpApplyIdQryExe; private final CpApplyStatusQryExe cpApplyStatusQryExe; + private final CpGiftRelationCheckQryExe cpGiftRelationCheckQryExe; private final SendCpApplyCmdExe sendCpApplyCmdExe; private final ProcessCpApplyCmd processCpApplyCmd; private final DismissCpApplyCmdExe dismissCpApplyCmdExe; @@ -96,7 +100,12 @@ public class UserCpRelationServiceImpl implements UserCpRelationService { } @Override - public CpCabinUserProfileCO getCpUserV4(UserIdCommonCmd cmd) { + public CpGiftRelationCheckCO checkGiftRelation(CpGiftRelationCheckCmd cmd) { + return cpGiftRelationCheckQryExe.execute(cmd); + } + + @Override + public CpCabinUserProfileCO getCpUserV4(UserIdCommonCmd cmd) { return userCpRelationQueryV4Exe.execute(cmd); } diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpGiftRelationCheckQryExeTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpGiftRelationCheckQryExeTest.java new file mode 100644 index 00000000..c7614af3 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpGiftRelationCheckQryExeTest.java @@ -0,0 +1,119 @@ +package com.red.circle.other.app.command.user.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpGiftRelationCheckCO; +import com.red.circle.other.app.dto.cmd.user.relation.cp.CpGiftRelationCheckCmd; +import com.red.circle.other.infra.database.rds.entity.user.user.CpRelationship; +import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipService; +import org.junit.jupiter.api.Test; + +class CpGiftRelationCheckQryExeTest { + + @Test + void shouldReturnOkWhenBothUsersHaveAvailableCpSlot() { + CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); + CpGiftRelationCheckQryExe exe = new CpGiftRelationCheckQryExe(cpRelationshipService); + + CpGiftRelationCheckCO result = exe.execute(cmd(1001L, 2001L, "CP")); + + assertTrue(result.getStatus()); + assertEquals("OK", result.getMsg()); + assertEquals("CP", result.getRelationType()); + assertEquals(0, result.getUserRelationCount()); + assertEquals(0, result.getAcceptUserRelationCount()); + assertEquals(1, result.getMaxRelationCount()); + assertFalse(result.getUserRelationFull()); + assertFalse(result.getAcceptUserRelationFull()); + } + + @Test + void shouldReturnFalseWhenSenderRelationshipIsFull() { + CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); + when(cpRelationshipService.countCp(1001L, "CP")).thenReturn(1); + CpGiftRelationCheckQryExe exe = new CpGiftRelationCheckQryExe(cpRelationshipService); + + CpGiftRelationCheckCO result = exe.execute(cmd(1001L, 2001L, "CP")); + + assertFalse(result.getStatus()); + assertEquals("Your relationships are full.", result.getMsg()); + assertEquals(1, result.getUserRelationCount()); + assertEquals(0, result.getAcceptUserRelationCount()); + assertTrue(result.getUserRelationFull()); + assertFalse(result.getAcceptUserRelationFull()); + } + + @Test + void shouldReturnFalseWhenReceiverRelationshipIsFull() { + CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); + when(cpRelationshipService.countCp(1001L, "BROTHER")).thenReturn(2); + when(cpRelationshipService.countCp(2001L, "BROTHER")).thenReturn(3); + CpGiftRelationCheckQryExe exe = new CpGiftRelationCheckQryExe(cpRelationshipService); + + CpGiftRelationCheckCO result = exe.execute(cmd(1001L, 2001L, "BROTHER")); + + assertFalse(result.getStatus()); + assertEquals("The other party's relationships are full.", result.getMsg()); + assertEquals(2, result.getUserRelationCount()); + assertEquals(3, result.getAcceptUserRelationCount()); + assertEquals(3, result.getMaxRelationCount()); + assertFalse(result.getUserRelationFull()); + assertTrue(result.getAcceptUserRelationFull()); + } + + @Test + void shouldNotBlockGiftWhenUsersAlreadyHaveRelationship() { + CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); + when(cpRelationshipService.countCp(1001L, "CP")).thenReturn(1); + when(cpRelationshipService.countCp(2001L, "CP")).thenReturn(1); + when(cpRelationshipService.existsRelationship(1001L, 2001L)).thenReturn(true); + CpGiftRelationCheckQryExe exe = new CpGiftRelationCheckQryExe(cpRelationshipService); + + CpGiftRelationCheckCO result = exe.execute(cmd(1001L, 2001L, "CP")); + + assertTrue(result.getStatus()); + assertEquals("OK", result.getMsg()); + assertTrue(result.getUserRelationFull()); + assertTrue(result.getAcceptUserRelationFull()); + } + + @Test + void shouldNotBlockGiftWhenRelationIsDismissing() { + CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); + when(cpRelationshipService.countCp(1001L, "SISTERS")).thenReturn(3); + when(cpRelationshipService.countCp(2001L, "SISTERS")).thenReturn(3); + when(cpRelationshipService.getDismissingCp(1001L, 2001L, "SISTERS")) + .thenReturn(new CpRelationship()); + CpGiftRelationCheckQryExe exe = new CpGiftRelationCheckQryExe(cpRelationshipService); + + CpGiftRelationCheckCO result = exe.execute(cmd(1001L, 2001L, "SISTERS")); + + assertTrue(result.getStatus()); + assertEquals("OK", result.getMsg()); + } + + @Test + void shouldReturnFalseForUnsupportedRelationTypeWithoutThrowing() { + CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class); + CpGiftRelationCheckQryExe exe = new CpGiftRelationCheckQryExe(cpRelationshipService); + + CpGiftRelationCheckCO result = exe.execute(cmd(1001L, 2001L, "FRIEND")); + + assertFalse(result.getStatus()); + assertEquals("relationType is not supported.", result.getMsg()); + assertEquals("FRIEND", result.getRelationType()); + } + + private static CpGiftRelationCheckCmd cmd(Long reqUserId, Long acceptUserId, + String relationType) { + CpGiftRelationCheckCmd cmd = new CpGiftRelationCheckCmd(); + cmd.setReqUserId(reqUserId); + cmd.setAcceptUserId(acceptUserId); + cmd.setRelationType(relationType); + return cmd; + } +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/user/relation/cp/CpGiftRelationCheckCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/user/relation/cp/CpGiftRelationCheckCO.java new file mode 100644 index 00000000..118a7eee --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/user/relation/cp/CpGiftRelationCheckCO.java @@ -0,0 +1,69 @@ +package com.red.circle.other.app.dto.clientobject.user.relation.cp; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.red.circle.framework.dto.ClientObject; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * CP gift relationship limit check result. + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class CpGiftRelationCheckCO extends ClientObject { + + /** + * Whether app can continue sending the relationship gift. + */ + private Boolean status; + + /** + * App display message. + */ + private String msg; + + /** + * Relationship type: CP/BROTHER/SISTERS. + */ + private String relationType; + + /** + * Gift sender user id. + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long userId; + + /** + * Gift receiver user id. + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long acceptUserId; + + /** + * Sender current normal relationship count for relationType. + */ + private Integer userRelationCount; + + /** + * Receiver current normal relationship count for relationType. + */ + private Integer acceptUserRelationCount; + + /** + * Max count for relationType: CP=1, BROTHER/SISTERS=3. + */ + private Integer maxRelationCount; + + /** + * Whether sender reached maxRelationCount. + */ + private Boolean userRelationFull; + + /** + * Whether receiver reached maxRelationCount. + */ + private Boolean acceptUserRelationFull; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/user/relation/cp/CpGiftRelationCheckCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/user/relation/cp/CpGiftRelationCheckCmd.java new file mode 100644 index 00000000..1b49e165 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/user/relation/cp/CpGiftRelationCheckCmd.java @@ -0,0 +1,29 @@ +package com.red.circle.other.app.dto.cmd.user.relation.cp; + +import com.red.circle.common.business.dto.cmd.AppExtCommand; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * CP gift relationship limit check. + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class CpGiftRelationCheckCmd extends AppExtCommand { + + /** + * Gift receiver user id. + * + * @eo.required + */ + @NotNull(message = "acceptUserId required.") + private Long acceptUserId; + + /** + * Relationship type carried by the CP gift: CP/BROTHER/SISTERS, default CP. + */ + private String relationType; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/user/relation/UserCpRelationService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/user/relation/UserCpRelationService.java index c52933c7..62dfbf63 100644 --- a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/user/relation/UserCpRelationService.java +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/user/relation/UserCpRelationService.java @@ -4,15 +4,17 @@ import com.red.circle.common.business.dto.cmd.UserIdCmd; import com.red.circle.common.business.dto.cmd.UserIdCommonCmd; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpCabinUserProfileCO; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpDismissApplyCO; +import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpGiftRelationCheckCO; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpPairUserProfileCO; import com.red.circle.other.app.dto.clientobject.user.relation.cp.CpUserProfileCO; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyIdQueryCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyDismissCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpApplyStatusQueryCmd; +import com.red.circle.other.app.dto.cmd.user.relation.cp.CpGiftRelationCheckCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.CpRelationshipQueryCmd; import com.red.circle.other.app.dto.cmd.user.relation.cp.ProcessApplyCmd; -import com.red.circle.other.app.dto.cmd.user.relation.cp.loveletter.SendLoveLetterCmd; +import com.red.circle.other.app.dto.cmd.user.relation.cp.loveletter.SendLoveLetterCmd; import java.util.List; @@ -37,6 +39,8 @@ public interface UserCpRelationService { String getCpApplyStatus(CpApplyStatusQueryCmd cmd); + CpGiftRelationCheckCO checkGiftRelation(CpGiftRelationCheckCmd cmd); + CpCabinUserProfileCO getCpUserV4(UserIdCommonCmd cmd); void sendLoveLetter(SendLoveLetterCmd cmd); diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/enums/user/user/CpRelationshipType.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/enums/user/user/CpRelationshipType.java index 87e5f0de..f6e08d51 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/enums/user/user/CpRelationshipType.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/enums/user/user/CpRelationshipType.java @@ -56,4 +56,11 @@ public enum CpRelationshipType { case SISTERS -> "sisters"; }; } + + public int maxCount() { + return switch (this) { + case CP -> 1; + case BROTHER, SISTERS -> 3; + }; + } } diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java b/rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java index 9656bf8b..4bd8ef87 100644 --- a/rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java +++ b/rc-service/rc-service-other/other-inner-endpoint/src/test/java/com/red/circle/other/app/inner/service/team/impl/TeamManualSalaryPaymentClientServiceImplTest.java @@ -1,14 +1,45 @@ package com.red.circle.other.app.inner.service.team.impl; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +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.component.redis.service.RedisService; +import com.red.circle.framework.dto.ResultResponse; import com.red.circle.other.infra.database.mongo.dto.team.TeamBillMemberTarget; +import com.red.circle.other.infra.database.mongo.dto.team.TeamPolicy; import com.red.circle.other.infra.database.mongo.dto.team.TeamSalaryPaymentDetails; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMemberTarget; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamPolicyManager; +import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile; +import com.red.circle.other.infra.database.mongo.service.team.TeamBillCycleUtils; +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.TeamPolicyManagerService; +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.entity.team.team.TeamSalaryPayment; +import com.red.circle.other.inner.enums.team.TeamMemberTargetIndex; +import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentCmd; +import com.red.circle.other.inner.model.dto.agency.agency.TeamManualSalaryPaymentResultDTO; +import com.red.circle.wallet.inner.endpoint.bank.UserBankBalanceClient; +import com.red.circle.wallet.inner.endpoint.bank.UserBankRunningWaterClient; +import com.red.circle.wallet.inner.model.dto.BankBalanceDTO; import java.lang.reflect.Constructor; import java.math.BigDecimal; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; class TeamManualSalaryPaymentClientServiceImplTest { @@ -79,6 +110,111 @@ class TeamManualSalaryPaymentClientServiceImplTest { "salary")); } + @Test + void payShouldReturnTopUpDetailsWhenCurrentLevelRecordExistsButSalaryIsInsufficient() { + RedisService redisService = mock(RedisService.class); + TeamMemberService teamMemberService = mock(TeamMemberService.class); + TeamProfileService teamProfileService = mock(TeamProfileService.class); + UserBankBalanceClient userBankBalanceClient = mock(UserBankBalanceClient.class); + TeamMemberTargetService teamMemberTargetService = mock(TeamMemberTargetService.class); + TeamSalaryPaymentService teamSalaryPaymentService = mock(TeamSalaryPaymentService.class); + TeamPolicyManagerService teamPolicyManagerService = mock(TeamPolicyManagerService.class); + UserBankRunningWaterClient userBankRunningWaterClient = mock(UserBankRunningWaterClient.class); + TeamManualSalaryPaymentClientServiceImpl service = new TeamManualSalaryPaymentClientServiceImpl( + redisService, teamMemberService, teamProfileService, userBankBalanceClient, + teamMemberTargetService, teamSalaryPaymentService, teamPolicyManagerService, + userBankRunningWaterClient); + + String sysOrigin = "LIKEI"; + String countryCode = "PH"; + String region = "2046067717605224449"; + Long userId = 2054591808909930498L; + Long teamId = 2054614353419759617L; + Integer billBelong = TeamBillCycleUtils.getCalcBillBelong(); + String paymentId = userId + "_" + billBelong; + TeamMemberTarget target = new TeamMemberTarget() + .setId("target-id") + .setTimeId(2056024761217118210L) + .setSysOrigin(sysOrigin) + .setRegion(region) + .setCountryCode(countryCode) + .setTeamId(teamId) + .setUserId(userId) + .setBillBelong(billBelong) + .setDailyTargets(List.of(new TeamMemberTargetIndex() + .setOwnOnlineTime(717L) + .setAcceptGiftValue(15151819L))); + TeamSalaryPayment existingPayment = new TeamSalaryPayment() + .setId(paymentId) + .setUserId(userId) + .setDateNumber(billBelong) + .setSalarySendDetails(List.of( + new TeamSalaryPaymentDetails() + .setTeamMemberId(userId) + .setTeamOwnId(userId) + .setTeamRegionId(region) + .setPolicyLevel(6) + .setAnchor(Boolean.TRUE) + .setSalary(new BigDecimal("22.00")) + .setRecycled(Boolean.FALSE), + new TeamSalaryPaymentDetails() + .setTeamMemberId(userId) + .setTeamOwnId(userId) + .setTeamRegionId(region) + .setPolicyLevel(6) + .setAnchor(Boolean.FALSE) + .setSalary(new BigDecimal("9.50")) + .setRecycled(Boolean.FALSE))); + TeamPolicyManager policyManager = new TeamPolicyManager() + .setSysOrigin(sysOrigin) + .setRegion(region) + .setCountryCode(countryCode) + .setPolicy(List.of(new TeamPolicy() + .setLevel(6) + .setTarget(8810000L) + .setOnlineTime(10L) + .setEffectiveDay(0) + .setMemberSalary(new BigDecimal("35.00")) + .setOwnSalary(new BigDecimal("8.75")) + .setTotalSalary(new BigDecimal("43.75")))); + + when(redisService.lock(anyString(), anyLong())).thenReturn(true); + when(teamMemberTargetService.listManualSalaryTargets(eq(sysOrigin), eq(countryCode), anySet(), + eq(billBelong))).thenReturn(List.of(target)); + when(teamMemberService.mapByMemberIds(anySet())).thenReturn(Map.of(userId, + new TeamMember().setMemberId(userId).setTeamId(teamId))); + when(teamProfileService.mapProfileByIds(anySet())).thenReturn(Map.of(teamId, + new TeamProfile().setId(teamId).setOwnUserId(userId).setRegion(region))); + when(teamSalaryPaymentService.listByPaymentOrRecipientUserIdsAndDateNumbers(eq(sysOrigin), + anySet(), anySet())).thenReturn(List.of(existingPayment)); + when(teamPolicyManagerService.getReleaseByRegionAndType(sysOrigin, region, null, countryCode)) + .thenReturn(policyManager); + when(teamSalaryPaymentService.createIfAbsent(userId, billBelong, sysOrigin)) + .thenReturn(new TeamSalaryPayment().setId(paymentId)); + when(userBankBalanceClient.getBalance(userId)) + .thenReturn(ResultResponse.success(BankBalanceDTO.of(0L))) + .thenReturn(ResultResponse.success(BankBalanceDTO.of(1225L))); + when(userBankBalanceClient.incr(any())).thenReturn(ResultResponse.success(Boolean.TRUE)); + when(userBankRunningWaterClient.add(any())).thenReturn(ResultResponse.success()); + when(teamSalaryPaymentService.mapPolicy(paymentId)).thenReturn(new HashMap<>()); + + List results = service.pay(new TeamManualSalaryPaymentCmd() + .setSysOrigin(sysOrigin) + .setCountryCode(countryCode) + .setUserIds(List.of(userId))); + + assertEquals(1, results.size()); + assertEquals(userId, results.get(0).getUserId()); + assertEquals(new BigDecimal("12.25"), results.get(0).getIssuedSalary()); + ArgumentCaptor detailsCaptor = + ArgumentCaptor.forClass(TeamSalaryPaymentDetails.class); + verify(teamSalaryPaymentService).paymentSalary(eq(paymentId), detailsCaptor.capture()); + assertEquals(new BigDecimal("12.25"), detailsCaptor.getValue().getSalary()); + assertEquals(Boolean.TRUE, detailsCaptor.getValue().getAnchor()); + verify(teamSalaryPaymentService, never()).checkRepeatPayment(anyString(), anyLong(), + anyString(), any(), any()); + } + private Object paymentSummary(BigDecimal totalIssuedSalary, BigDecimal memberIssuedSalary, BigDecimal agentIssuedSalary) throws Exception { Class summaryClass = Class.forName( From 65732197513c3d53fe50054285fa7d04a15d83a1 Mon Sep 17 00:00:00 2001 From: hy001 Date: Mon, 25 May 2026 17:07:13 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=81=E7=A4=BC?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/command/user/ProcessCpApplyCmd.java | 14 +++++++++-- .../user/query/CpApplyStatusQryExe.java | 1 - .../command/user/ProcessCpApplyCmdTest.java | 18 ++++++++++++++ .../user/query/CpApplyStatusQryExeTest.java | 5 ++-- .../user/user/impl/CpApplyServiceImpl.java | 24 +++++++------------ 5 files changed, 42 insertions(+), 20 deletions(-) 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 fd0fc6b3..6d255bfd 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 @@ -19,6 +19,7 @@ import com.red.circle.other.infra.enums.user.user.CpApplyStatus; import com.red.circle.other.infra.enums.user.user.CpRelationshipType; import com.red.circle.other.inner.asserts.user.UserRelationErrorCode; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -40,6 +41,7 @@ public class ProcessCpApplyCmd { private static final int CP_MAX_COUNT = 1; private static final int FRIEND_MAX_COUNT = 3; + private static final long APPLY_WAIT_EXPIRE_HOURS = 24L; private final CpApplyService cpApplyService; private final CpValueService cpValueService; @@ -60,8 +62,9 @@ public class ProcessCpApplyCmd { String relationType = CpRelationshipType.normalize(cpApply.getRelationType()); // 状态错误 - ResponseAssert.isTrue(CommonErrorCode.STATE_ERROR, - Objects.equals(cpApply.getStatus(), CpApplyStatus.WAIT.name())); + ResponseAssert.isTrue(CommonErrorCode.STATE_ERROR, + Objects.equals(cpApply.getStatus(), CpApplyStatus.WAIT.name())); + ResponseAssert.isFalse(CommonErrorCode.STATE_ERROR, isOverdueWait(cpApply)); boolean agree = isAgree(cmd); boolean isReconcile = false; @@ -114,6 +117,13 @@ public class ProcessCpApplyCmd { sendProcessNotice(cpApply, true); } + private boolean isOverdueWait(CpApply cpApply) { + return Objects.equals(cpApply.getStatus(), CpApplyStatus.WAIT.name()) + && Objects.nonNull(cpApply.getCreateTime()) + && cpApply.getCreateTime().toLocalDateTime() + .isBefore(LocalDateTime.now().minusHours(APPLY_WAIT_EXPIRE_HOURS)); + } + private void sendProcessNotice(CpApply cpApply, boolean agree) { try { UserProfile senderProfile = userProfileGateway.getByUserId(cpApply.getSendApplyUserId()); diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExe.java index 3eea2de8..04cc9489 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExe.java @@ -26,7 +26,6 @@ public class CpApplyStatusQryExe { return null; } if (isOverdueWait(cpApply)) { - cpApplyService.expireWaitById(cpApply.getId()); return CpApplyStatus.EXPIRE.name(); } return cpApply.getStatus(); 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 f732b9af..028b0100 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 @@ -22,6 +22,8 @@ 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.infra.database.rds.service.user.user.CpValueService; import com.red.circle.other.infra.enums.user.user.CpApplyStatus; +import java.sql.Timestamp; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; @@ -84,6 +86,22 @@ class ProcessCpApplyCmdTest { verify(fixture.imMessageClient, never()).sendCustomMessage(any(CustomC2cMsgBodyCmd.class)); } + @Test + void processOverdueWaitApplyShouldFailStateError() { + Fixture fixture = new Fixture("CP"); + CpApply overdueApply = apply("CP"); + overdueApply.setCreateTime(Timestamp.valueOf(LocalDateTime.now().minusHours(25))); + when(fixture.cpApplyService.getById(3001L)).thenReturn(overdueApply); + + 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.imMessageClient, never()).sendCustomMessage(any(CustomC2cMsgBodyCmd.class)); + } + @Test void agreeApplyShouldAutoRefuseOtherWaitAppliesBetweenSameUsersWithoutRejectedIm() { Fixture fixture = new Fixture("CP"); diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExeTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExeTest.java index 11ea8ada..b798d7ad 100644 --- a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExeTest.java +++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/command/user/query/CpApplyStatusQryExeTest.java @@ -3,6 +3,7 @@ package com.red.circle.other.app.command.user.query; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,7 +35,7 @@ class CpApplyStatusQryExeTest { } @Test - void shouldExpireOverdueWaitApplyWhenQueryingStatus() { + void shouldReturnExpireForOverdueWaitApplyWithoutPersistingStatus() { CpApplyService cpApplyService = mock(CpApplyService.class); CpApply cpApply = new CpApply() .setId(3001L) @@ -45,7 +46,7 @@ class CpApplyStatusQryExeTest { CpApplyStatusQryExe exe = new CpApplyStatusQryExe(cpApplyService); assertEquals("EXPIRE", exe.execute(cmd(3001L))); - verify(cpApplyService).expireWaitById(3001L); + verify(cpApplyService, never()).expireWaitById(3001L); } private static CpApplyStatusQueryCmd cmd(Long applyId) { 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 9a9aadae..9f886577 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 @@ -111,27 +111,21 @@ public class CpApplyServiceImpl extends BaseServiceImpl imp @Override public boolean expireWaitById(Long id) { - return update() - .set(CpApply::getStatus, CpApplyStatus.EXPIRE.name()) - .set(CpApply::getUpdateTime, LocalDateTime.now()) + return Optional.ofNullable(query() + .select(CpApply::getId) .eq(CpApply::getId, id) .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) - .execute(); + .lt(CpApply::getCreateTime, waitExpireBefore()) + .last(PageConstant.LIMIT_ONE) + .getOne()) + .map(cpApply -> Objects.nonNull(cpApply.getId())) + .orElse(Boolean.FALSE); } @Override public void expireOverdueWaitApplies(Long userId) { - if (Objects.isNull(userId)) { - return; - } - update() - .set(CpApply::getStatus, CpApplyStatus.EXPIRE.name()) - .set(CpApply::getUpdateTime, LocalDateTime.now()) - .eq(CpApply::getStatus, CpApplyStatus.WAIT.name()) - .lt(CpApply::getCreateTime, waitExpireBefore()) - .and(where -> where.eq(CpApply::getSendApplyUserId, userId) - .or().eq(CpApply::getAcceptApplyUserId, userId)) - .execute(); + // The production status column does not accept a persisted EXPIRE value. + // Expiration is derived from create_time so old WAIT rows do not block new applies. } private Timestamp waitExpireBefore() {