fix(send-routing): address final review findings

- Persist conversationScene in create/update conversation
- Make bindAccount validate by conversation scene (1=WECOM_BOT, 2=WECHAT_PERSONAL)
- Re-derive channelType from conversation_scene in dispatchRecord
- Check WeCom response errcode and HTTP status in WeComBotSender
- Support custom webhook_url from ext_info in WeComBotSender
- Configure RestTemplate with 5s connect / 10s read timeouts
- Expand WeComBotSenderTest with MockRestServiceServer coverage
This commit is contained in:
2026-07-14 18:35:50 +08:00
parent e1c7c582ee
commit 11ee7384b3
6 changed files with 198 additions and 17 deletions

View File

@@ -21,6 +21,7 @@ public class CreateConversationRequest {
private Long ownerAccountId;
private Long subjectId;
private String conversationName;
private Integer conversationScene;
private Integer createSource;
private Map<String, Object> receiveConfig;
}

View File

@@ -13,6 +13,7 @@ public class UpdateConversationRequest {
private Long ownerAccountId;
private Long subjectId;
private Integer conversationScene;
private Integer status;
private Map<String, Object> receiveConfig;
}

View File

@@ -10,6 +10,7 @@ import com.sino.mci.channel.dto.UpdateConversationRequest;
import com.sino.mci.channel.entity.ChannelAccount;
import com.sino.mci.channel.entity.ChannelAccountConversation;
import com.sino.mci.channel.entity.Conversation;
import com.sino.mci.channel.common.model.AccountType;
import com.sino.mci.channel.entity.ConversationParticipant;
import com.sino.mci.channel.entity.ConversationTagBinding;
import com.sino.mci.channel.repository.ChannelAccountConversationMapper;
@@ -78,6 +79,7 @@ public class ConversationService {
conversation.setOwnerAccountId(request.getOwnerAccountId());
conversation.setSubjectId(request.getSubjectId());
conversation.setConversationName(request.getConversationName());
conversation.setConversationScene(request.getConversationScene());
conversation.setCreateSource(request.getCreateSource() != null ? request.getCreateSource() : 2);
conversation.setReceiveConfig(JsonUtils.toJson(request.getReceiveConfig()));
conversation.setStatus(1);
@@ -109,6 +111,9 @@ public class ConversationService {
if (request.getReceiveConfig() != null) {
conversation.setReceiveConfig(JsonUtils.toJson(request.getReceiveConfig()));
}
if (request.getConversationScene() != null) {
conversation.setConversationScene(request.getConversationScene());
}
conversationMapper.updateById(conversation);
return conversation;
}
@@ -127,7 +132,7 @@ public class ConversationService {
if (account == null || !tenantCode.equals(account.getTenantCode())) {
throw new BusinessException("账号不存在或不在当前租户");
}
if (!conversation.getChannelType().equalsIgnoreCase(account.getChannelType())) {
if (!isAccountCompatibleWithConversation(conversation, account)) {
throw new BusinessException("账号渠道类型与会话不一致");
}
@@ -144,6 +149,24 @@ public class ConversationService {
binding.setSortOrder(request.getSortOrder() != null ? request.getSortOrder() : 0);
binding.setStatus(1);
bindingMapper.insert(binding);
// 主发送账号作为会话负责人
if (request.getBindingType() != null && request.getBindingType() == 1) {
conversation.setOwnerAccountId(request.getAccountId());
conversationMapper.updateById(conversation);
}
}
private boolean isAccountCompatibleWithConversation(Conversation conversation, ChannelAccount account) {
Integer scene = conversation.getConversationScene();
if (scene != null) {
return switch (scene) {
case 1 -> AccountType.WECOM_BOT.getCode() == account.getAccountType();
case 2 -> AccountType.WECHAT_PERSONAL.getCode() == account.getAccountType();
default -> conversation.getChannelType().equalsIgnoreCase(account.getChannelType());
};
}
return conversation.getChannelType().equalsIgnoreCase(account.getChannelType());
}
@Transactional

View File

@@ -853,6 +853,22 @@ public class SendService {
public void dispatchRecord(SendRecord record) {
ChannelType channelType = parseChannelType(record.getChannelType());
Set<Integer> accountTypes;
if (record.getConversationId() != null) {
Conversation conversation = conversationMapper.selectById(record.getConversationId());
Integer scene = conversation != null ? conversation.getConversationScene() : null;
if (scene != null) {
channelType = resolveChannelTypeForScene(scene);
accountTypes = resolveAccountTypesForScene(scene);
} else {
accountTypes = null;
}
} else if (record.getPersonId() != null) {
accountTypes = ACCOUNT_TYPES_WECOM_AGENT;
} else {
accountTypes = null;
}
ChannelAdapter adapter = adapterRegistry.getAdapter(channelType);
if (adapter == null) {
record.setSendStatus(SEND_STATUS_FAILED);
@@ -875,15 +891,6 @@ public class SendService {
return;
}
Set<Integer> accountTypes;
if (record.getConversationId() != null) {
Conversation conversation = conversationMapper.selectById(record.getConversationId());
accountTypes = resolveAccountTypesForScene(conversation != null ? conversation.getConversationScene() : null);
} else if (record.getPersonId() != null) {
accountTypes = ACCOUNT_TYPES_WECOM_AGENT;
} else {
accountTypes = null;
}
List<Long> candidateAccountIds = selectSendAccountCandidates(record, channelType, accountTypes);
if (candidateAccountIds.isEmpty()) {
record.setSendStatus(SEND_STATUS_FAILED);