diff --git a/backend/mci-server/src/test/java/com/sino/mci/send/service/SendRoutingIntegrationTest.java b/backend/mci-server/src/test/java/com/sino/mci/send/service/SendRoutingIntegrationTest.java new file mode 100644 index 0000000..a249a87 --- /dev/null +++ b/backend/mci-server/src/test/java/com/sino/mci/send/service/SendRoutingIntegrationTest.java @@ -0,0 +1,327 @@ +package com.sino.mci.send.service; + +import com.sino.mci.channel.common.model.AccountType; +import com.sino.mci.channel.common.model.ChannelType; +import com.sino.mci.channel.entity.ChannelAccount; +import com.sino.mci.channel.entity.ChannelAccountConversation; +import com.sino.mci.channel.entity.ChannelSubject; +import com.sino.mci.channel.entity.Conversation; +import com.sino.mci.channel.repository.ChannelAccountConversationMapper; +import com.sino.mci.channel.repository.ChannelAccountMapper; +import com.sino.mci.channel.repository.ChannelSubjectMapper; +import com.sino.mci.channel.repository.ConversationMapper; +import com.sino.mci.crm.entity.ChannelIdentity; +import com.sino.mci.crm.entity.Person; +import com.sino.mci.crm.repository.ChannelIdentityMapper; +import com.sino.mci.crm.repository.PersonMapper; +import com.sino.mci.send.entity.SendRecord; +import com.sino.mci.send.repository.SendRecordMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +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.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import static com.sino.mci.test.AuthTestHelper.createTenantAndLogin; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * 基于会话场景与目标类型的发送路由集成测试。 + * + *

覆盖自动路由策略: + *

+ */ +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@TestPropertySource(properties = { + "spring.rabbitmq.listener.simple.auto-startup=false" +}) +class SendRoutingIntegrationTest { + + private static final String CHANNEL_TYPE_WECOM = ChannelType.WECOM.name().toLowerCase(); + private static final String CHANNEL_TYPE_WECHAT_PERSONAL = ChannelType.WECHAT_PERSONAL.name().toLowerCase(); + + private static final int CONVERSATION_SCENE_INTERNAL = 1; + private static final int CONVERSATION_SCENE_EXTERNAL = 2; + + private static final int PERSON_TYPE_INTERNAL = 1; + private static final int PERSON_TYPE_EXTERNAL = 3; + + private static final int STATUS_ENABLED = 1; + private static final int BINDING_TYPE_SEND_PRIMARY = 1; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ChannelSubjectMapper subjectMapper; + + @Autowired + private ChannelAccountMapper accountMapper; + + @Autowired + private ConversationMapper conversationMapper; + + @Autowired + private ChannelAccountConversationMapper accountConversationMapper; + + @Autowired + private PersonMapper personMapper; + + @Autowired + private ChannelIdentityMapper channelIdentityMapper; + + @Autowired + private SendRecordMapper sendRecordMapper; + + @Test + void shouldRouteInternalGroupToWeComBot() throws Exception { + String tenantCode = "routing-int-group-" + UUID.randomUUID().toString().substring(0, 8); + String authToken = createTenantAndLogin(mockMvc, tenantCode, "内部群路由测试"); + + ChannelSubject subject = createWeComSubject(tenantCode); + ChannelAccount botAccount = createWeComBotAccount(tenantCode, subject.getId()); + Conversation conversation = createConversation(tenantCode, subject.getId(), + CONVERSATION_SCENE_INTERNAL, "内部群"); + bindAccountToConversation(botAccount.getId(), conversation.getId()); + + String taskId = sendTestMessage(authToken, tenantCode, + buildConversationPayload(conversation.getId())); + + List records = sendRecordMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SendRecord::getTenantCode, tenantCode) + .eq(SendRecord::getTaskId, taskId)); + + assertThat(records).hasSize(1); + SendRecord record = records.get(0); + assertThat(record.getChannelType()).isEqualTo(CHANNEL_TYPE_WECOM); + assertThat(record.getAccountId()).isEqualTo(botAccount.getId()); + assertThat(record.getConversationId()).isEqualTo(conversation.getId()); + } + + @Test + void shouldRouteExternalGroupToWeChatPersonal() throws Exception { + String tenantCode = "routing-ext-group-" + UUID.randomUUID().toString().substring(0, 8); + String authToken = createTenantAndLogin(mockMvc, tenantCode, "外部群路由测试"); + + ChannelSubject subject = createWeComSubject(tenantCode); + ChannelAccount pywechatAccount = createWeChatPersonalAccount(tenantCode, subject.getId()); + Conversation conversation = createConversation(tenantCode, subject.getId(), + CONVERSATION_SCENE_EXTERNAL, "外部群"); + bindAccountToConversation(pywechatAccount.getId(), conversation.getId()); + + String taskId = sendTestMessage(authToken, tenantCode, + buildConversationPayload(conversation.getId())); + + List records = sendRecordMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SendRecord::getTenantCode, tenantCode) + .eq(SendRecord::getTaskId, taskId)); + + assertThat(records).hasSize(1); + SendRecord record = records.get(0); + assertThat(record.getChannelType()).isEqualTo(CHANNEL_TYPE_WECHAT_PERSONAL); + assertThat(record.getAccountId()).isEqualTo(pywechatAccount.getId()); + assertThat(record.getConversationId()).isEqualTo(conversation.getId()); + } + + @Test + void shouldRouteInternalPersonToWeComAgent() throws Exception { + String tenantCode = "routing-int-person-" + UUID.randomUUID().toString().substring(0, 8); + String authToken = createTenantAndLogin(mockMvc, tenantCode, "内部人员路由测试"); + + ChannelSubject subject = createWeComSubject(tenantCode); + ChannelAccount agentAccount = createWeComAgentAccount(tenantCode, subject.getId()); + Person person = createPerson(tenantCode, "int-user", "内部人员", PERSON_TYPE_INTERNAL); + createChannelIdentity(tenantCode, person.getId(), subject.getId(), "int-user"); + + String taskId = sendTestMessage(authToken, tenantCode, + buildPersonPayload(person.getId())); + + List records = sendRecordMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SendRecord::getTenantCode, tenantCode) + .eq(SendRecord::getTaskId, taskId)); + + assertThat(records).hasSize(1); + SendRecord record = records.get(0); + assertThat(record.getChannelType()).isEqualTo(CHANNEL_TYPE_WECOM); + assertThat(record.getAccountId()).isEqualTo(agentAccount.getId()); + assertThat(record.getPersonId()).isEqualTo(person.getId()); + } + + @Test + void shouldRouteExternalPersonToWeComAgent() throws Exception { + String tenantCode = "routing-ext-person-" + UUID.randomUUID().toString().substring(0, 8); + String authToken = createTenantAndLogin(mockMvc, tenantCode, "外部人员路由测试"); + + ChannelSubject subject = createWeComSubject(tenantCode); + ChannelAccount agentAccount = createWeComAgentAccount(tenantCode, subject.getId()); + Person person = createPerson(tenantCode, "ext-user", "外部人员", PERSON_TYPE_EXTERNAL); + createChannelIdentity(tenantCode, person.getId(), subject.getId(), "ext-user"); + + String taskId = sendTestMessage(authToken, tenantCode, + buildPersonPayload(person.getId())); + + List records = sendRecordMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(SendRecord::getTenantCode, tenantCode) + .eq(SendRecord::getTaskId, taskId)); + + assertThat(records).hasSize(1); + SendRecord record = records.get(0); + assertThat(record.getChannelType()).isEqualTo(CHANNEL_TYPE_WECOM); + assertThat(record.getAccountId()).isEqualTo(agentAccount.getId()); + assertThat(record.getPersonId()).isEqualTo(person.getId()); + } + + private String sendTestMessage(String authToken, String tenantCode, String payload) throws Exception { + String response = mockMvc.perform(post("/msg-platform/api/v1/send/test") + .contextPath("/msg-platform") + .header("Authorization", authToken) + .header("X-Tenant-Code", tenantCode) + .contentType(MediaType.APPLICATION_JSON) + .content(payload)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.totalCount").value(1)) + .andExpect(jsonPath("$.data.status").value("simulated")) + .andReturn().getResponse().getContentAsString(); + return com.jayway.jsonpath.JsonPath.read(response, "$.data.taskId"); + } + + private String buildConversationPayload(Long conversationId) { + return String.format( + "{\"targetType\":\"conversation\",\"targetValue\":{\"conversation_id\":%d},\"contentType\":\"text\",\"content\":{\"text\":\"hello\"},\"channelStrategy\":{\"channel_type\":\"wecom\",\"strategy\":\"primary\"}}", + conversationId); + } + + private String buildPersonPayload(Long personId) { + return String.format( + "{\"targetType\":\"person\",\"targetValue\":{\"person_id\":%d},\"contentType\":\"text\",\"content\":{\"text\":\"hello\"},\"channelStrategy\":{\"channel_type\":\"wecom\"}}", + personId); + } + + private ChannelSubject createWeComSubject(String tenantCode) { + ChannelSubject subject = new ChannelSubject(); + subject.setTenantCode(tenantCode); + subject.setChannelType(CHANNEL_TYPE_WECOM); + subject.setSubjectCode("sub-" + UUID.randomUUID().toString().substring(0, 8)); + subject.setSubjectName("测试主体"); + subject.setCorpId("corp-" + UUID.randomUUID().toString().substring(0, 8)); + subject.setAgentId("1000002"); + subject.setAgentSecret("secret-" + UUID.randomUUID()); + subject.setStatus(STATUS_ENABLED); + subjectMapper.insert(subject); + return subject; + } + + private ChannelAccount createWeComBotAccount(String tenantCode, Long subjectId) { + ChannelAccount account = new ChannelAccount(); + account.setTenantCode(tenantCode); + account.setAccountType(AccountType.WECOM_BOT.getCode()); + account.setChannelType(CHANNEL_TYPE_WECOM); + account.setSubjectId(subjectId); + account.setAccountId("bot-" + UUID.randomUUID().toString().substring(0, 8)); + account.setAccountName("企微群机器人"); + account.setStatus(STATUS_ENABLED); + accountMapper.insert(account); + return account; + } + + private ChannelAccount createWeComAgentAccount(String tenantCode, Long subjectId) { + ChannelAccount account = new ChannelAccount(); + account.setTenantCode(tenantCode); + account.setAccountType(AccountType.WECOM_AGENT.getCode()); + account.setChannelType(CHANNEL_TYPE_WECOM); + account.setSubjectId(subjectId); + account.setAccountId("agent-" + UUID.randomUUID().toString().substring(0, 8)); + account.setAccountName("企微应用"); + account.setStatus(STATUS_ENABLED); + accountMapper.insert(account); + return account; + } + + private ChannelAccount createWeChatPersonalAccount(String tenantCode, Long subjectId) { + ChannelAccount account = new ChannelAccount(); + account.setTenantCode(tenantCode); + account.setAccountType(AccountType.WECHAT_PERSONAL.getCode()); + account.setChannelType(CHANNEL_TYPE_WECHAT_PERSONAL); + account.setSubjectId(subjectId); + account.setAccountId("wxid-" + UUID.randomUUID().toString().substring(0, 8)); + account.setAccountName("pywechat 个人号"); + account.setStatus(STATUS_ENABLED); + account.setOnlineStatus(STATUS_ENABLED); + account.setRiskLevel(1); + account.setLastHeartbeat(LocalDateTime.now()); + accountMapper.insert(account); + return account; + } + + private Conversation createConversation(String tenantCode, Long subjectId, + Integer conversationScene, String conversationName) { + Conversation conversation = new Conversation(); + conversation.setTenantCode(tenantCode); + conversation.setConversationType(2); + conversation.setChannelType(CHANNEL_TYPE_WECOM); + conversation.setChannelConversationId("chat-" + UUID.randomUUID().toString().substring(0, 8)); + conversation.setSubjectId(subjectId); + conversation.setConversationName(conversationName); + conversation.setConversationScene(conversationScene); + conversation.setCreateSource(1); + conversation.setStatus(STATUS_ENABLED); + conversationMapper.insert(conversation); + return conversation; + } + + private void bindAccountToConversation(Long accountId, Long conversationId) { + ChannelAccountConversation binding = new ChannelAccountConversation(); + binding.setAccountId(accountId); + binding.setConversationId(conversationId); + binding.setBindingType(BINDING_TYPE_SEND_PRIMARY); + binding.setSortOrder(1); + binding.setStatus(STATUS_ENABLED); + accountConversationMapper.insert(binding); + } + + private Person createPerson(String tenantCode, String personCode, String personName, Integer personType) { + Person person = new Person(); + person.setTenantCode(tenantCode); + person.setPersonCode(personCode + "-" + UUID.randomUUID().toString().substring(0, 8)); + person.setPersonName(personName); + person.setPhone("13800138000"); + person.setPersonType(personType); + person.setStatus(STATUS_ENABLED); + personMapper.insert(person); + return person; + } + + private void createChannelIdentity(String tenantCode, Long personId, Long subjectId, String channelUserId) { + ChannelIdentity identity = new ChannelIdentity(); + identity.setTenantCode(tenantCode); + identity.setPersonId(personId); + identity.setChannelType(CHANNEL_TYPE_WECOM); + identity.setChannelUserId(channelUserId + "-" + UUID.randomUUID().toString().substring(0, 8)); + identity.setChannelUserName("测试用户"); + identity.setSubjectId(subjectId); + identity.setStatus(STATUS_ENABLED); + channelIdentityMapper.insert(identity); + } +}