From fc6dbb2ca35fae95df3082da1a160077320c99a7 Mon Sep 17 00:00:00 2001 From: marsal Date: Wed, 8 Jul 2026 17:42:51 +0800 Subject: [PATCH] fix(send): apply AccountSelector health checks to fallback owner account and add person-target validation tests - Make AccountSelector.isHealthy public static so SendService can reuse it. - Use isHealthy for the resolved/fallback conversation account instead of a weaker existence/status/channel check, covering risk level and PYWECHAT heartbeat/online status. - Add null-safe channel-type comparison in resolvePersonTarget. - Add @Min(0) to BindConversationAccountRequest.sortOrder. - Rename misleading conversation fallback test and add tests for resolvePersonTarget disabled/cross-tenant/channel-mismatch cases. - Remove unused java.util.Arrays import from test file. --- .../dto/BindConversationAccountRequest.java | 2 + .../mci/channel/service/AccountSelector.java | 2 +- .../sino/mci/send/service/SendService.java | 6 +- .../service/SendServiceResolveTargetTest.java | 72 ++++++++++++++++++- 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/dto/BindConversationAccountRequest.java b/backend/mci-server/src/main/java/com/sino/mci/channel/dto/BindConversationAccountRequest.java index deffcb7..bef7b2e 100644 --- a/backend/mci-server/src/main/java/com/sino/mci/channel/dto/BindConversationAccountRequest.java +++ b/backend/mci-server/src/main/java/com/sino/mci/channel/dto/BindConversationAccountRequest.java @@ -1,5 +1,6 @@ package com.sino.mci.channel.dto; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.hibernate.validator.constraints.Range; @@ -14,5 +15,6 @@ public class BindConversationAccountRequest { @Range(min = 1, max = 4) private Integer bindingType; + @Min(0) private Integer sortOrder; } diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/service/AccountSelector.java b/backend/mci-server/src/main/java/com/sino/mci/channel/service/AccountSelector.java index a0313c3..e17a5a0 100644 --- a/backend/mci-server/src/main/java/com/sino/mci/channel/service/AccountSelector.java +++ b/backend/mci-server/src/main/java/com/sino/mci/channel/service/AccountSelector.java @@ -71,7 +71,7 @@ public class AccountSelector { return healthyAccountIds; } - private boolean isHealthy(ChannelAccount account, String tenantCode, ChannelType channelType) { + public static boolean isHealthy(ChannelAccount account, String tenantCode, ChannelType channelType) { if (account == null) { return false; } diff --git a/backend/mci-server/src/main/java/com/sino/mci/send/service/SendService.java b/backend/mci-server/src/main/java/com/sino/mci/send/service/SendService.java index 37aa0bf..30577d8 100644 --- a/backend/mci-server/src/main/java/com/sino/mci/send/service/SendService.java +++ b/backend/mci-server/src/main/java/com/sino/mci/send/service/SendService.java @@ -336,9 +336,7 @@ public class SendService { throw new BusinessException("会话未配置发送账号: " + conversationId); } ChannelAccount account = accountMapper.selectById(accountId); - if (account == null || !tenantCode.equals(account.getTenantCode()) - || !channelType.name().equalsIgnoreCase(account.getChannelType()) - || account.getStatus() == null || account.getStatus() != STATUS_ENABLED) { + if (!AccountSelector.isHealthy(account, tenantCode, channelType)) { throw new BusinessException("会话没有可用的发送账号: " + conversationId); } SendTarget target = new SendTarget(); @@ -370,7 +368,7 @@ public class SendService { || account.getStatus() == null || account.getStatus() != STATUS_ENABLED) { throw new BusinessException("租户没有可用的发送账号: channel=" + channelType.name().toLowerCase()); } - if (!channelType.name().equalsIgnoreCase(account.getChannelType())) { + if (!Objects.equals(channelType.name().toLowerCase(), account.getChannelType())) { throw new BusinessException("单聊发送账号渠道类型与目标身份不一致"); } SendTarget target = new SendTarget(); diff --git a/backend/mci-server/src/test/java/com/sino/mci/send/service/SendServiceResolveTargetTest.java b/backend/mci-server/src/test/java/com/sino/mci/send/service/SendServiceResolveTargetTest.java index 736b67d..80e79d6 100644 --- a/backend/mci-server/src/test/java/com/sino/mci/send/service/SendServiceResolveTargetTest.java +++ b/backend/mci-server/src/test/java/com/sino/mci/send/service/SendServiceResolveTargetTest.java @@ -10,6 +10,7 @@ import com.sino.mci.channel.repository.ConversationMapper; import com.sino.mci.channel.repository.ConversationTagBindingMapper; import com.sino.mci.channel.service.AccountSelector; import com.sino.mci.crm.repository.ChannelIdentityMapper; +import com.sino.mci.crm.entity.ChannelIdentity; import com.sino.mci.crm.repository.PersonMapper; import com.sino.mci.crm.repository.PersonTagBindingMapper; import com.sino.mci.crm.repository.TagMapper; @@ -26,7 +27,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -108,7 +108,7 @@ class SendServiceResolveTargetTest { } @Test - void resolveConversationTarget_fallsBackToBackupWhenPrimaryUnhealthy() { + void resolveConversationTarget_usesAccountReturnedBySelector() { Conversation conversation = createConversation(); Long backupAccountId = 20L; @@ -143,6 +143,49 @@ class SendServiceResolveTargetTest { assertEquals("backup", strategyCaptor.getValue()); } + @Test + void resolvePersonTarget_rejectsDisabledAccount() { + ChannelIdentity identity = createIdentity(); + ChannelAccount account = createAccount(30L, "wecom", 0); + + when(channelIdentityMapper.selectOne(any())).thenReturn(identity); + when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account)); + when(accountMapper.selectById(30L)).thenReturn(account); + + BusinessException ex = assertThrows(BusinessException.class, + () -> sendService.resolveTestTargets("tenant", createPersonRequest())); + assertEquals("租户没有可用的发送账号: channel=wecom", ex.getMessage()); + } + + @Test + void resolvePersonTarget_rejectsCrossTenantAccount() { + ChannelIdentity identity = createIdentity(); + ChannelAccount account = createAccount(30L, "wecom", 1); + account.setTenantCode("other"); + + when(channelIdentityMapper.selectOne(any())).thenReturn(identity); + when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account)); + when(accountMapper.selectById(30L)).thenReturn(account); + + BusinessException ex = assertThrows(BusinessException.class, + () -> sendService.resolveTestTargets("tenant", createPersonRequest())); + assertEquals("租户没有可用的发送账号: channel=wecom", ex.getMessage()); + } + + @Test + void resolvePersonTarget_rejectsChannelTypeMismatch() { + ChannelIdentity identity = createIdentity(); + ChannelAccount account = createAccount(30L, "mobile", 1); + + when(channelIdentityMapper.selectOne(any())).thenReturn(identity); + when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account)); + when(accountMapper.selectById(30L)).thenReturn(account); + + BusinessException ex = assertThrows(BusinessException.class, + () -> sendService.resolveTestTargets("tenant", createPersonRequest())); + assertEquals("单聊发送账号渠道类型与目标身份不一致", ex.getMessage()); + } + private SendTestRequest createConversationRequest(String strategy) { SendTestRequest request = new SendTestRequest(); request.setTargetType("conversation"); @@ -159,6 +202,21 @@ class SendServiceResolveTargetTest { return request; } + private SendTestRequest createPersonRequest() { + SendTestRequest request = new SendTestRequest(); + request.setTargetType("person"); + Map targetValue = new HashMap<>(); + targetValue.put("person_id", 1L); + request.setTargetValue(targetValue); + request.setContentType("text"); + request.setContent(Collections.singletonMap("text", "hello")); + + Map channelStrategy = new HashMap<>(); + channelStrategy.put("channel_type", "wecom"); + request.setChannelStrategy(channelStrategy); + return request; + } + private Conversation createConversation() { Conversation conversation = new Conversation(); conversation.setId(1L); @@ -177,4 +235,14 @@ class SendServiceResolveTargetTest { account.setStatus(status); return account; } + + private ChannelIdentity createIdentity() { + ChannelIdentity identity = new ChannelIdentity(); + identity.setTenantCode("tenant"); + identity.setPersonId(1L); + identity.setChannelType("wecom"); + identity.setChannelUserId("user-1"); + identity.setStatus(1); + return identity; + } }