feat(channel-wecom): 实现企微会话存档接收骨架(Finance SDK 接口 + 回调 + 异步拉取)

This commit is contained in:
2026-07-07 19:42:49 +08:00
parent 7edbb0a178
commit 7b16160bba
11 changed files with 845 additions and 1 deletions

View File

@@ -0,0 +1,226 @@
package com.sino.mci.channel.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sino.mci.channel.entity.ChannelSubject;
import com.sino.mci.channel.messaging.RabbitConfig;
import com.sino.mci.channel.messaging.SessionArchivePullCommand;
import com.sino.mci.channel.repository.ChannelSubjectMapper;
import com.sino.mci.channel.service.SessionArchivePullService;
import com.sino.mci.channel.wecom.archive.FinanceSdkClient;
import com.sino.mci.channel.wecom.archive.model.FinanceMessage;
import com.sino.mci.inbound.entity.MessageEvent;
import com.sino.mci.inbound.repository.MessageEventMapper;
import com.sino.mci.message.entity.MessageMedia;
import com.sino.mci.message.repository.MessageMediaMapper;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static com.sino.mci.test.AuthTestHelper.createTenantAndLogin;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
"spring.rabbitmq.listener.simple.auto-startup=false",
"spring.main.allow-bean-definition-overriding=true",
"sino.channel.wecom.session-archive.listener-enabled=false"
})
@Transactional
class WeComSessionArchiveTest {
private static final String CORP_ID = "ww-session-archive";
@Autowired
private MockMvc mockMvc;
@Autowired
private ChannelSubjectMapper channelSubjectMapper;
@Autowired
private MessageEventMapper messageEventMapper;
@Autowired
private MessageMediaMapper messageMediaMapper;
@Autowired
private SessionArchivePullService sessionArchivePullService;
@Autowired
private FinanceSdkClient financeSdkClient;
@Autowired
private RabbitTemplate rabbitTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void shouldReceiveMsgAuditNotifyAndPublishPullCommand() throws Exception {
ChannelSubject subject = createSessionArchiveSubject();
String xml = """
<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[sys]]></FromUserName>
<CreateTime>1700000000</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[msgaudit_notify]]></Event>
</xml>
""".formatted(CORP_ID);
mockMvc.perform(post("/msg-platform/callback/wecom")
.contextPath("/msg-platform")
.contentType(MediaType.APPLICATION_XML)
.content(xml))
.andExpect(status().isOk())
.andExpect(content().string("success"));
ArgumentCaptor<SessionArchivePullCommand> captor = ArgumentCaptor.forClass(SessionArchivePullCommand.class);
verify(rabbitTemplate).convertAndSend(eq(RabbitConfig.SESSION_ARCHIVE_EXCHANGE),
eq(RabbitConfig.SESSION_ARCHIVE_ROUTING_KEY), captor.capture());
SessionArchivePullCommand command = captor.getValue();
assertThat(command.getChannelSubjectId()).isEqualTo(subject.getId());
assertThat(command.getCorpId()).isEqualTo(CORP_ID);
assertThat(command.getTenantCode()).isEqualTo(subject.getTenantCode());
}
@Test
void shouldPullAndSaveMessageEvent() throws Exception {
ChannelSubject subject = createSessionArchiveSubject();
when(financeSdkClient.init(anyString(), anyString())).thenReturn(true);
when(financeSdkClient.getChatData(anyLong(), anyInt())).thenReturn(List.of(
FinanceMessage.builder()
.seq(100L)
.msgId("msg-100")
.publicKeyVer(1)
.encryptRandomKey("random-100")
.encryptChatMsg("chat-100")
.build()
));
when(financeSdkClient.decryptData(anyString(), anyString())).thenReturn(
objectMapper.writeValueAsString(Map.of(
"msgid", "msg-100",
"msgtype", "text",
"text", Map.of("content", "hello")
))
);
SessionArchivePullCommand command = new SessionArchivePullCommand();
command.setChannelSubjectId(subject.getId());
command.setTenantCode(subject.getTenantCode());
command.setCorpId(subject.getCorpId());
command.setSecret(subject.getCorpSecret());
command.setPrivateKey(subject.getSessionArchivePrivateKey());
sessionArchivePullService.onPullCommand(command);
MessageEvent event = messageEventMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MessageEvent>()
.eq(MessageEvent::getEventId, "msg-100"));
assertThat(event).isNotNull();
assertThat(event.getMessageSource()).isEqualTo("session_archive");
assertThat(event.getChannelSubjectId()).isEqualTo(subject.getId());
assertThat(event.getChannelType()).isEqualTo("wecom");
assertThat(event.getContent()).contains("hello");
}
@Test
void shouldSaveMediaRecordForImageMessage() throws Exception {
ChannelSubject subject = createSessionArchiveSubject();
when(financeSdkClient.init(anyString(), anyString())).thenReturn(true);
when(financeSdkClient.getChatData(anyLong(), anyInt())).thenReturn(List.of(
FinanceMessage.builder()
.seq(200L)
.msgId("msg-200")
.publicKeyVer(1)
.encryptRandomKey("random-200")
.encryptChatMsg("chat-200")
.build()
));
when(financeSdkClient.decryptData(anyString(), anyString())).thenReturn(
objectMapper.writeValueAsString(Map.of(
"msgid", "msg-200",
"msgtype", "image",
"image", Map.of(
"sdkfileid", "sdkfileid-200",
"filesize", 1024,
"md5sum", "abc123"
)
))
);
SessionArchivePullCommand command = new SessionArchivePullCommand();
command.setChannelSubjectId(subject.getId());
command.setTenantCode(subject.getTenantCode());
command.setCorpId(subject.getCorpId());
command.setSecret(subject.getCorpSecret());
command.setPrivateKey(subject.getSessionArchivePrivateKey());
sessionArchivePullService.onPullCommand(command);
MessageMedia media = messageMediaMapper.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MessageMedia>()
.eq(MessageMedia::getMsgid, "msg-200"));
assertThat(media).isNotNull();
assertThat(media.getSdkfileid()).isEqualTo("sdkfileid-200");
assertThat(media.getMediaType()).isEqualTo("image");
assertThat(media.getStatus()).isEqualTo(0);
assertThat(media.getFileSize()).isEqualTo(1024L);
}
private ChannelSubject createSessionArchiveSubject() throws Exception {
String tenantCode = "sa-" + UUID.randomUUID().toString().substring(0, 8);
createTenantAndLogin(mockMvc, tenantCode, "会话存档租户");
ChannelSubject subject = new ChannelSubject();
subject.setTenantCode(tenantCode);
subject.setChannelType("wecom");
subject.setSubjectCode("archive");
subject.setSubjectName("会话存档主体");
subject.setCorpId(CORP_ID);
subject.setCorpSecret("secret-" + UUID.randomUUID());
subject.setSessionArchiveEnabled(1);
subject.setSessionArchivePrivateKey("private-key");
subject.setStatus(1);
channelSubjectMapper.insert(subject);
return subject;
}
@TestConfiguration
static class MockConfig {
@Bean
public FinanceSdkClient financeSdkClient() {
return mock(FinanceSdkClient.class);
}
@Bean
public RabbitTemplate rabbitTemplate() {
return mock(RabbitTemplate.class);
}
}
}