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:
@@ -1,12 +1,16 @@
|
||||
package com.sino.mci.channel.wecom;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sino.mci.channel.common.dto.SendMessageRequest;
|
||||
import com.sino.mci.channel.common.dto.SendMessageResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.HttpStatusCodeException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -17,8 +21,21 @@ import java.util.Map;
|
||||
public class WeComBotSender {
|
||||
|
||||
private static final String DEFAULT_WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s";
|
||||
private static final int CONNECT_TIMEOUT_MS = 5000;
|
||||
private static final int READ_TIMEOUT_MS = 10000;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public WeComBotSender() {
|
||||
this.restTemplate = createRestTemplate();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
public WeComBotSender(RestTemplate restTemplate, ObjectMapper objectMapper) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public SendMessageResult send(SendMessageRequest request) {
|
||||
SendMessageResult result = new SendMessageResult();
|
||||
@@ -42,9 +59,21 @@ public class WeComBotSender {
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
|
||||
try {
|
||||
String url = String.format(DEFAULT_WEBHOOK_URL, key);
|
||||
restTemplate.postForEntity(url, entity, String.class);
|
||||
String url = resolveWebhookUrl(request.getExtra(), key);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
||||
Map<String, Object> responseBody = parseResponseBody(response.getBody());
|
||||
int errcode = parseInt(responseBody.get("errcode"));
|
||||
if (errcode != 0) {
|
||||
result.setSuccess(false);
|
||||
String errmsg = getString(responseBody, "errmsg");
|
||||
result.setMessage("企微群机器人发送失败: [" + errcode + "] " + errmsg);
|
||||
return result;
|
||||
}
|
||||
result.setSuccess(true);
|
||||
} catch (HttpStatusCodeException e) {
|
||||
log.warn("[WeComBot] 发送失败,HTTP 状态: {}", e.getStatusCode(), e);
|
||||
result.setSuccess(false);
|
||||
result.setMessage("企微群机器人发送失败,HTTP 状态: " + e.getStatusCode());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeComBot] 发送失败", e);
|
||||
result.setSuccess(false);
|
||||
@@ -53,6 +82,47 @@ public class WeComBotSender {
|
||||
return result;
|
||||
}
|
||||
|
||||
private String resolveWebhookUrl(Map<String, Object> extra, String key) {
|
||||
String customUrl = getString(extra, "webhook_url");
|
||||
if (customUrl != null && !customUrl.isBlank()) {
|
||||
return String.format(customUrl, key);
|
||||
}
|
||||
return String.format(DEFAULT_WEBHOOK_URL, key);
|
||||
}
|
||||
|
||||
private Map<String, Object> parseResponseBody(String body) {
|
||||
if (body == null || body.isBlank()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(body, Map.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeComBot] 解析响应失败: {}", body, e);
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
private int parseInt(Object value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private RestTemplate createRestTemplate() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
factory.setReadTimeout(READ_TIMEOUT_MS);
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildBody(String contentType, Map<String, Object> content) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
switch (contentType) {
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
package com.sino.mci.channel.wecom;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sino.mci.channel.common.dto.SendMessageRequest;
|
||||
import com.sino.mci.channel.common.dto.SendMessageResult;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
|
||||
|
||||
class WeComBotSenderTest {
|
||||
|
||||
private final WeComBotSender sender = new WeComBotSender();
|
||||
private RestTemplate restTemplate;
|
||||
private MockRestServiceServer server;
|
||||
private WeComBotSender sender;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
restTemplate = new RestTemplate();
|
||||
server = MockRestServiceServer.createServer(restTemplate);
|
||||
sender = new WeComBotSender(restTemplate, new ObjectMapper());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendRejectsMissingKey() {
|
||||
@@ -22,12 +40,73 @@ class WeComBotSenderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendBuildsTextBody() {
|
||||
void sendBuildsTextBody() throws Exception {
|
||||
server.expect(requestTo("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy"))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.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"));
|
||||
request.setContentType("text");
|
||||
request.setContent(Map.of("text", "hello"));
|
||||
// With a real key this hits WeCom; keep as shape validation only.
|
||||
assertNotNull(request);
|
||||
SendMessageResult result = sender.send(request);
|
||||
assertTrue(result.isSuccess(), result.getMessage());
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendRejectsUnsupportedContentType() {
|
||||
SendMessageRequest request = new SendMessageRequest();
|
||||
request.setExtra(Map.of("account_id", "dummy"));
|
||||
request.setContentType("image");
|
||||
request.setContent(Map.of("media_id", "xxx"));
|
||||
SendMessageResult result = sender.send(request);
|
||||
assertFalse(result.isSuccess());
|
||||
assertTrue(result.getMessage().contains("不支持的消息类型"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendReportsWeComErrorResponse() {
|
||||
server.expect(requestTo("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy"))
|
||||
.andRespond(withSuccess("{\"errcode\":40008,\"errmsg\":\"invalid message type\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
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("40008"));
|
||||
assertTrue(result.getMessage().contains("invalid message type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendReportsHttpErrorStatus() {
|
||||
server.expect(requestTo("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=dummy"))
|
||||
.andRespond(withStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR));
|
||||
|
||||
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("HTTP 状态"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendUsesCustomWebhookUrl() throws Exception {
|
||||
server.expect(requestTo("https://custom.example.com/webhook/send?key=dummy"))
|
||||
.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://custom.example.com/webhook/send?key=%s"));
|
||||
request.setContentType("text");
|
||||
request.setContent(Map.of("text", "hello"));
|
||||
SendMessageResult result = sender.send(request);
|
||||
assertTrue(result.isSuccess(), result.getMessage());
|
||||
server.verify();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user