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;
|
package com.sino.mci.channel.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.hibernate.validator.constraints.Range;
|
import org.hibernate.validator.constraints.Range;
|
||||||
@@ -14,5 +15,6 @@ public class BindConversationAccountRequest {
|
|||||||
@Range(min = 1, max = 4)
|
@Range(min = 1, max = 4)
|
||||||
private Integer bindingType;
|
private Integer bindingType;
|
||||||
|
|
||||||
|
@Min(0)
|
||||||
private Integer sortOrder;
|
private Integer sortOrder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public class AccountSelector {
|
|||||||
return healthyAccountIds;
|
return healthyAccountIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isHealthy(ChannelAccount account, String tenantCode, ChannelType channelType) {
|
public static boolean isHealthy(ChannelAccount account, String tenantCode, ChannelType channelType) {
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,9 +336,7 @@ public class SendService {
|
|||||||
throw new BusinessException("会话未配置发送账号: " + conversationId);
|
throw new BusinessException("会话未配置发送账号: " + conversationId);
|
||||||
}
|
}
|
||||||
ChannelAccount account = accountMapper.selectById(accountId);
|
ChannelAccount account = accountMapper.selectById(accountId);
|
||||||
if (account == null || !tenantCode.equals(account.getTenantCode())
|
if (!AccountSelector.isHealthy(account, tenantCode, channelType)) {
|
||||||
|| !channelType.name().equalsIgnoreCase(account.getChannelType())
|
|
||||||
|| account.getStatus() == null || account.getStatus() != STATUS_ENABLED) {
|
|
||||||
throw new BusinessException("会话没有可用的发送账号: " + conversationId);
|
throw new BusinessException("会话没有可用的发送账号: " + conversationId);
|
||||||
}
|
}
|
||||||
SendTarget target = new SendTarget();
|
SendTarget target = new SendTarget();
|
||||||
@@ -370,7 +368,7 @@ public class SendService {
|
|||||||
|| account.getStatus() == null || account.getStatus() != STATUS_ENABLED) {
|
|| account.getStatus() == null || account.getStatus() != STATUS_ENABLED) {
|
||||||
throw new BusinessException("租户没有可用的发送账号: channel=" + channelType.name().toLowerCase());
|
throw new BusinessException("租户没有可用的发送账号: channel=" + channelType.name().toLowerCase());
|
||||||
}
|
}
|
||||||
if (!channelType.name().equalsIgnoreCase(account.getChannelType())) {
|
if (!Objects.equals(channelType.name().toLowerCase(), account.getChannelType())) {
|
||||||
throw new BusinessException("单聊发送账号渠道类型与目标身份不一致");
|
throw new BusinessException("单聊发送账号渠道类型与目标身份不一致");
|
||||||
}
|
}
|
||||||
SendTarget target = new SendTarget();
|
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.repository.ConversationTagBindingMapper;
|
||||||
import com.sino.mci.channel.service.AccountSelector;
|
import com.sino.mci.channel.service.AccountSelector;
|
||||||
import com.sino.mci.crm.repository.ChannelIdentityMapper;
|
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.PersonMapper;
|
||||||
import com.sino.mci.crm.repository.PersonTagBindingMapper;
|
import com.sino.mci.crm.repository.PersonTagBindingMapper;
|
||||||
import com.sino.mci.crm.repository.TagMapper;
|
import com.sino.mci.crm.repository.TagMapper;
|
||||||
@@ -26,7 +27,6 @@ import org.mockito.InjectMocks;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -108,7 +108,7 @@ class SendServiceResolveTargetTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveConversationTarget_fallsBackToBackupWhenPrimaryUnhealthy() {
|
void resolveConversationTarget_usesAccountReturnedBySelector() {
|
||||||
Conversation conversation = createConversation();
|
Conversation conversation = createConversation();
|
||||||
Long backupAccountId = 20L;
|
Long backupAccountId = 20L;
|
||||||
|
|
||||||
@@ -143,6 +143,49 @@ class SendServiceResolveTargetTest {
|
|||||||
assertEquals("backup", strategyCaptor.getValue());
|
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) {
|
private SendTestRequest createConversationRequest(String strategy) {
|
||||||
SendTestRequest request = new SendTestRequest();
|
SendTestRequest request = new SendTestRequest();
|
||||||
request.setTargetType("conversation");
|
request.setTargetType("conversation");
|
||||||
@@ -159,6 +202,21 @@ class SendServiceResolveTargetTest {
|
|||||||
return request;
|
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() {
|
private Conversation createConversation() {
|
||||||
Conversation conversation = new Conversation();
|
Conversation conversation = new Conversation();
|
||||||
conversation.setId(1L);
|
conversation.setId(1L);
|
||||||
@@ -177,4 +235,14 @@ class SendServiceResolveTargetTest {
|
|||||||
account.setStatus(status);
|
account.setStatus(status);
|
||||||
return account;
|
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