package com.tcm.training.admin.live.service; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.tcm.training.admin.live.dto.LiveSessionQuestionRequest; import com.tcm.training.admin.live.dto.LiveSessionUpsertRequest; import com.tcm.training.admin.live.entity.LiveCourse; import com.tcm.training.admin.live.entity.LiveSession; import com.tcm.training.admin.live.entity.LiveSessionQuestion; import com.tcm.training.admin.live.mapper.LiveCourseMapper; import com.tcm.training.admin.live.mapper.LiveReserveMapper; import com.tcm.training.admin.live.mapper.LiveSessionMapper; import com.tcm.training.admin.live.mapper.LiveSessionQuestionMapper; import com.tcm.training.admin.live.vo.LiveSessionVO; import com.tcm.training.admin.live.vo.LiveSessionStatusVO; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * 直播场次服务测试。 */ @ExtendWith(MockitoExtension.class) class LiveSessionAppServiceTest { @BeforeAll static void initLambdaCache() { MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); TableInfoHelper.initTableInfo(assistant, LiveCourse.class); TableInfoHelper.initTableInfo(assistant, LiveSession.class); TableInfoHelper.initTableInfo(assistant, LiveSessionQuestion.class); } @Mock private LiveCourseMapper liveCourseMapper; @Mock private LiveSessionMapper liveSessionMapper; @Mock private LiveSessionQuestionMapper liveSessionQuestionMapper; @Mock private LiveReserveMapper liveReserveMapper; private LiveSessionAppService liveSessionAppService; @Captor private ArgumentCaptor questionCaptor; @Mock private LiveRemindDispatchService liveRemindDispatchService; /** * 每个用例前重建被测服务。 */ @BeforeEach void setUp() { liveSessionAppService = new LiveSessionAppService( liveCourseMapper, liveSessionMapper, liveSessionQuestionMapper, liveReserveMapper, liveRemindDispatchService ); } /** * 编辑场次题目时应先物理删除旧题,避免逻辑删除记录触发唯一索引冲突。 */ @Test void updateSessionShouldHardDeleteQuestionsBeforeReinsert() { LiveCourse course = new LiveCourse(); course.setId(9000L); LiveSession session = new LiveSession(); session.setId(90008L); session.setLiveCourseId(9000L); session.setLiveStatus(0); session.setStartTime(LocalDateTime.now().plusDays(1)); when(liveCourseMapper.selectById(9000L)).thenReturn(course); when(liveSessionMapper.selectById(90008L)).thenReturn(session); when(liveSessionQuestionMapper.selectList(any())).thenReturn(Collections.emptyList()); LiveSessionUpsertRequest request = new LiveSessionUpsertRequest(); request.setStartTime(LocalDateTime.now().plusDays(2)); request.setVideoUrl("https://cdn.example/live.m3u8"); request.setDurationSeconds(1800); request.setBaseReserveCount(12); request.setBaseWatchCount(8); request.setQuestions(Arrays.asList( buildQuestion(1, "题目一"), buildQuestion(2, "题目二"), buildQuestion(3, "题目三") )); liveSessionAppService.updateSession(9000L, 90008L, request); verify(liveSessionQuestionMapper).hardDeleteBySessionId(90008L); verify(liveSessionQuestionMapper, never()).delete(any()); verify(liveSessionMapper).updateById(org.mockito.ArgumentMatchers.argThat(updatedSession -> updatedSession != null && Long.valueOf(90008L).equals(updatedSession.getId()) && Integer.valueOf(1800).equals(updatedSession.getDurationSeconds()))); verify(liveSessionQuestionMapper, org.mockito.Mockito.times(3)).insert(questionCaptor.capture()); assertEquals(3, questionCaptor.getAllValues().size()); assertEquals(Long.valueOf(90008L), questionCaptor.getAllValues().get(0).getSessionId()); assertEquals(Integer.valueOf(1), questionCaptor.getAllValues().get(0).getSort()); assertTrue(questionCaptor.getAllValues().stream().allMatch(item -> Long.valueOf(90008L).equals(item.getSessionId()))); } /** * 查询场次列表时应回传视频时长,供管理后台回显。 */ @Test void pageSessionsShouldIncludeDurationSeconds() { LiveCourse course = new LiveCourse(); course.setId(9000L); LiveSession session = new LiveSession(); session.setId(90008L); session.setLiveCourseId(9000L); session.setStartTime(LocalDateTime.of(2026, 4, 9, 19, 30)); session.setVideoUrl("https://cdn.example/live.mp4"); session.setDurationSeconds(2400); session.setLiveStatus(0); session.setBaseReserveCount(10); session.setBaseWatchCount(5); com.baomidou.mybatisplus.extension.plugins.pagination.Page page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page(1, 10); page.setRecords(Collections.singletonList(session)); page.setTotal(1); when(liveCourseMapper.selectById(9000L)).thenReturn(course); when(liveSessionMapper.selectPage(any(), any())).thenReturn(page); when(liveSessionQuestionMapper.selectList(any())).thenReturn(Collections.emptyList()); LiveSessionVO sessionVO = liveSessionAppService.pageSessions(9000L, 1, 10, null, null, null).getList().get(0); assertEquals(Integer.valueOf(2400), sessionVO.getDurationSeconds()); } /** * 手动开播时应复用公共状态迁移逻辑,并返回数据库中的最新开播时间。 */ @Test void startShouldMarkPendingSessionAsLiving() { LiveCourse course = new LiveCourse(); course.setId(9000L); LiveSession session = new LiveSession(); session.setId(90008L); session.setLiveCourseId(9000L); session.setLiveStatus(0); LiveSession startedSession = new LiveSession(); startedSession.setId(90008L); startedSession.setLiveCourseId(9000L); startedSession.setLiveStatus(1); startedSession.setActualStartTime(LocalDateTime.of(2026, 4, 9, 10, 0)); when(liveCourseMapper.selectById(9000L)).thenReturn(course); when(liveSessionMapper.selectById(90008L)).thenReturn(session, startedSession); when(liveSessionMapper.markLivingIfPending(org.mockito.ArgumentMatchers.eq(90008L), any(LocalDateTime.class))).thenReturn(1); LiveSessionStatusVO statusVO = liveSessionAppService.start(9000L, 90008L); assertEquals(Integer.valueOf(1), statusVO.getLiveStatus()); assertEquals(LocalDateTime.of(2026, 4, 9, 10, 0), statusVO.getActualStartTime()); } /** * 自动开播在状态已变化时应静默跳过,避免定时任务重复报错。 */ @Test void autoStartShouldReturnNullWhenStateTransitionFails() { LiveSession session = new LiveSession(); session.setId(90008L); session.setLiveCourseId(9000L); session.setLiveStatus(1); when(liveSessionMapper.selectById(90008L)).thenReturn(session); when(liveSessionMapper.markLivingIfPending(org.mockito.ArgumentMatchers.eq(90008L), any(LocalDateTime.class))).thenReturn(0); assertNull(liveSessionAppService.autoStart(90008L, LocalDateTime.of(2026, 4, 9, 10, 5))); } /** * 自动开播后应以后置查询结果为准,避免覆盖数据库中已有的实际开播时间。 */ @Test void autoStartShouldUsePersistedActualStartTime() { LiveSession session = new LiveSession(); session.setId(90008L); session.setLiveCourseId(9000L); session.setLiveStatus(0); LiveSession startedSession = new LiveSession(); startedSession.setId(90008L); startedSession.setLiveCourseId(9000L); startedSession.setLiveStatus(1); startedSession.setActualStartTime(LocalDateTime.of(2026, 4, 9, 9, 55)); when(liveSessionMapper.selectById(90008L)).thenReturn(session, startedSession); when(liveSessionMapper.markLivingIfPending(org.mockito.ArgumentMatchers.eq(90008L), any(LocalDateTime.class))).thenReturn(1); assertEquals( LocalDateTime.of(2026, 4, 9, 9, 55), liveSessionAppService.autoStart(90008L, LocalDateTime.of(2026, 4, 9, 10, 0)).getActualStartTime() ); } /** * 同课程时间线已推进到后续场次时,应将历史残留的直播中场次收敛为已结束。 */ @Test void reconcileCourseLivingSessionsShouldCloseHistoricalLivingSession() { LiveSession staleLivingSession = new LiveSession(); staleLivingSession.setId(90001L); staleLivingSession.setLiveCourseId(9000L); staleLivingSession.setLiveStatus(1); staleLivingSession.setStartTime(LocalDateTime.of(2026, 4, 8, 10, 0)); LiveSession latestEndedSession = new LiveSession(); latestEndedSession.setId(90002L); latestEndedSession.setLiveCourseId(9000L); latestEndedSession.setLiveStatus(2); latestEndedSession.setStartTime(LocalDateTime.of(2026, 4, 9, 10, 0)); latestEndedSession.setActualEndTime(LocalDateTime.of(2026, 4, 9, 10, 30)); when(liveSessionMapper.selectList(any())).thenReturn(Arrays.asList(staleLivingSession, latestEndedSession)); assertEquals(1, liveSessionAppService.reconcileCourseLivingSessions(9000L, LocalDateTime.of(2026, 4, 9, 10, 35))); verify(liveSessionMapper).updateById(org.mockito.ArgumentMatchers.argThat(session -> session != null && Long.valueOf(90001L).equals(session.getId()) && Integer.valueOf(2).equals(session.getLiveStatus()) && LocalDateTime.of(2026, 4, 9, 10, 35).equals(session.getActualEndTime()))); } /** * 构造题目请求。 */ private LiveSessionQuestionRequest buildQuestion(int sort, String stem) { LiveSessionQuestionRequest questionRequest = new LiveSessionQuestionRequest(); questionRequest.setSort(sort); questionRequest.setStem(stem); questionRequest.setOptionA("A"); questionRequest.setOptionB("B"); questionRequest.setOptionC("C"); questionRequest.setCorrectOption("A"); return questionRequest; } }