fix(send-routing): address final code review findings

- WeComBotSender.resolveWebhookUrl: use custom URL as-is when no %s placeholder
- WeComBotSender: treat unparseable/blank response body as failure
- ConversationService.isAccountCompatibleWithConversation: ignore scene for single chats
- SendService.selectSendAccountCandidates: apply accountTypes filter for non-conversation records
- SendService: replace account type magic numbers with AccountType constants
- WeComChannelAdapter: use AccountType.WECOM_BOT.getCode() instead of literal 6
- admin-web Conversation.vue: fallback to channelType filter when scene is unset
- add tests for single-chat null scene, webhook_url propagation, and custom URL without %s
This commit is contained in:
2026-07-15 11:53:51 +08:00
parent c24891a82e
commit 0a326e7ff9
8 changed files with 165 additions and 25 deletions

View File

@@ -62,6 +62,11 @@ public class WeComBotSender {
String url = resolveWebhookUrl(request.getExtra(), key);
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
Map<String, Object> responseBody = parseResponseBody(response.getBody());
if (responseBody == null) {
result.setSuccess(false);
result.setMessage("企微群机器人返回无法解析的响应");
return result;
}
int errcode = parseInt(responseBody.get("errcode"));
if (errcode != 0) {
result.setSuccess(false);
@@ -88,21 +93,20 @@ public class WeComBotSender {
if (customUrl.contains("%s")) {
return customUrl.replace("%s", key);
}
log.warn("[WeComBot] custom webhook_url missing %s placeholder; appending key param");
return customUrl.contains("?") ? customUrl + "&key=" + key : customUrl + "?key=" + key;
return customUrl;
}
return String.format(DEFAULT_WEBHOOK_URL, key);
}
private Map<String, Object> parseResponseBody(String body) {
if (body == null || body.isBlank()) {
return new HashMap<>();
return null;
}
try {
return objectMapper.readValue(body, Map.class);
} catch (Exception e) {
log.warn("[WeComBot] 解析响应失败: {}", body, e);
return new HashMap<>();
return null;
}
}

View File

@@ -5,6 +5,7 @@ import com.sino.mci.channel.common.dto.ChannelInitRequest;
import com.sino.mci.channel.common.dto.ChannelInitResult;
import com.sino.mci.channel.common.dto.SendMessageRequest;
import com.sino.mci.channel.common.dto.SendMessageResult;
import com.sino.mci.channel.common.model.AccountType;
import com.sino.mci.channel.common.model.ChannelType;
import com.sino.mci.channel.common.spi.ChannelAdapter;
import com.sino.mci.channel.wecom.support.WeComServiceFactory;
@@ -86,7 +87,7 @@ public class WeComChannelAdapter implements ChannelAdapter {
}
Integer accountType = getAccountType(request);
if (accountType != null && accountType == 6) {
if (accountType != null && accountType == AccountType.WECOM_BOT.getCode()) {
return weComBotSender.send(request);
}

View File

@@ -109,4 +109,33 @@ class WeComBotSenderTest {
assertTrue(result.isSuccess(), result.getMessage());
server.verify();
}
@Test
void sendUsesCustomWebhookUrlWithoutPlaceholder() throws Exception {
server.expect(requestTo("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REALKEY"))
.andExpect(content().json("{\"msgtype\":\"text\",\"text\":{\"content\":\"hello\"}}"))
.andRespond(withSuccess("{\"errcode\":0,\"errmsg\":\"ok\"}", MediaType.APPLICATION_JSON));
SendMessageRequest request = new SendMessageRequest();
request.setExtra(Map.of("account_id", "dummy", "webhook_url", "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REALKEY"));
request.setContentType("text");
request.setContent(Map.of("text", "hello"));
SendMessageResult result = sender.send(request);
assertTrue(result.isSuccess(), result.getMessage());
server.verify();
}
@Test
void sendFailsOnUnparseableResponseBody() {
server.expect(requestTo("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy"))
.andRespond(withSuccess("not-json", MediaType.TEXT_PLAIN));
SendMessageRequest request = new SendMessageRequest();
request.setExtra(Map.of("account_id", "dummy"));
request.setContentType("text");
request.setContent(Map.of("text", "hello"));
SendMessageResult result = sender.send(request);
assertFalse(result.isSuccess());
assertTrue(result.getMessage().contains("无法解析"));
}
}