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.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String, Object> targetValue = new HashMap<>();
|
||||
targetValue.put("person_id", 1L);
|
||||
request.setTargetValue(targetValue);
|
||||
request.setContentType("text");
|
||||
request.setContent(Collections.singletonMap("text", "hello"));
|
||||
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user