diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java b/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java index 815e811..223b930 100644 --- a/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java +++ b/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java @@ -18,7 +18,8 @@ public class AuthInterceptor implements HandlerInterceptor { || ("POST".equals(method) && isExactPath(path, "/api/v1/account/tenant")) || ("POST".equals(method) && isExactPath(path, "/api/v1/account/user")) || isExactPath(path, "/api/v1/health") - || ("POST".equals(method) && isExactPath(path, "/api/v1/channel-account/heartbeat"))) { + || ("POST".equals(method) && isExactPath(path, "/api/v1/channel-account/heartbeat")) + || ("POST".equals(method) && isExactPath(path, "/api/v1/channel-account/event"))) { return true; } diff --git a/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelAccountEventController.java b/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelAccountEventController.java new file mode 100644 index 0000000..d2cc507 --- /dev/null +++ b/backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelAccountEventController.java @@ -0,0 +1,25 @@ +package com.sino.mci.channel.controller; + +import com.sino.mci.channel.dto.ChannelAccountEventRequest; +import com.sino.mci.channel.service.ChannelAccountEventService; +import com.sino.mci.shared.web.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/channel-account") +@RequiredArgsConstructor +public class ChannelAccountEventController { + + private final ChannelAccountEventService eventService; + + @PostMapping("/event") + public ApiResponse receiveEvent(@Valid @RequestBody ChannelAccountEventRequest request) { + eventService.handleEvent(request); + return ApiResponse.ok(null); + } +} diff --git a/backend/mci-server/src/test/java/com/sino/mci/channel/controller/ChannelAccountEventControllerTest.java b/backend/mci-server/src/test/java/com/sino/mci/channel/controller/ChannelAccountEventControllerTest.java new file mode 100644 index 0000000..9430216 --- /dev/null +++ b/backend/mci-server/src/test/java/com/sino/mci/channel/controller/ChannelAccountEventControllerTest.java @@ -0,0 +1,87 @@ +package com.sino.mci.channel.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sino.mci.channel.dto.ChannelAccountEventRequest; +import com.sino.mci.channel.entity.ChannelAccount; +import com.sino.mci.channel.repository.ChannelAccountMapper; +import com.sino.mci.channel.repository.ChannelEventRecordMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Map; + +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 +@Sql(scripts = "/sql/test-channel-account.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +class ChannelAccountEventControllerTest { + + @Autowired + private MockMvc mockMvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private ChannelAccountMapper accountMapper; + + @Autowired + private ChannelEventRecordMapper eventRecordMapper; + + @Test + void shouldUpdateAccountOnHeartbeat() throws Exception { + ChannelAccountEventRequest request = new ChannelAccountEventRequest(); + request.setEventId("evt-001"); + request.setEventType("heartbeat"); + request.setAccountId("wx-bot-01"); + request.setTenantCode("rescue"); + request.setData(Map.of( + "login_status", "success", + "online_status", "online", + "risk_level", 10)); + + mockMvc.perform(post("/msg-platform/api/v1/channel-account/event") + .contextPath("/msg-platform") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)); + + ChannelAccount account = accountMapper.selectById(1L); + assertThat(account.getOnlineStatus()).isEqualTo(1); + assertThat(account.getRiskLevel()).isEqualTo(10); + assertThat(eventRecordMapper.selectById("evt-001")).isNotNull(); + } + + @Test + void shouldIgnoreDuplicateEvent() throws Exception { + ChannelAccountEventRequest request = new ChannelAccountEventRequest(); + request.setEventId("evt-dup-001"); + request.setEventType("heartbeat"); + request.setAccountId("wx-bot-01"); + request.setTenantCode("rescue"); + request.setData(Map.of("online_status", "online")); + + mockMvc.perform(post("/msg-platform/api/v1/channel-account/event") + .contextPath("/msg-platform") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + + mockMvc.perform(post("/msg-platform/api/v1/channel-account/event") + .contextPath("/msg-platform") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + + assertThat(eventRecordMapper.selectCount(null)).isEqualTo(1); + } +} diff --git a/backend/mci-server/src/test/resources/sql/test-channel-account.sql b/backend/mci-server/src/test/resources/sql/test-channel-account.sql new file mode 100644 index 0000000..27700a1 --- /dev/null +++ b/backend/mci-server/src/test/resources/sql/test-channel-account.sql @@ -0,0 +1,13 @@ +SET FOREIGN_KEY_CHECKS = 0; + +DELETE FROM mci_channel_event_record; +DELETE FROM mci_channel_account; +DELETE FROM mci_tenant; + +SET FOREIGN_KEY_CHECKS = 1; + +INSERT INTO mci_tenant (id, tenant_code, tenant_name, status, create_time) +VALUES (1, 'rescue', '救援', 1, NOW()); + +INSERT INTO mci_channel_account (id, tenant_code, account_type, channel_type, account_id, account_name, login_status, online_status, risk_level, status, create_time) +VALUES (1, 'rescue', 3, 'wechat_personal', 'wx-bot-01', '测试号', 0, 0, 0, 1, NOW());