package com.tcm.training.admin.app.live.service; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.tcm.training.admin.app.auth.service.AppAuthSessionService; import com.tcm.training.admin.app.live.support.AppLiveCourseDisplaySupport; import com.tcm.training.admin.app.live.vo.AppLiveCourseDetailVO; import com.tcm.training.admin.app.live.vo.AppLiveReserveResultVO; import com.tcm.training.admin.live.entity.Expert; import com.tcm.training.admin.live.entity.LiveCourse; import com.tcm.training.admin.live.entity.LiveReserve; import com.tcm.training.admin.live.entity.LiveSession; import com.tcm.training.admin.live.mapper.ExpertMapper; 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 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * APP 养生直播详情服务测试。 */ @ExtendWith(MockitoExtension.class) class AppLiveCourseServiceTest { @BeforeAll static void initLambdaCache() { MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); TableInfoHelper.initTableInfo(assistant, LiveCourse.class); TableInfoHelper.initTableInfo(assistant, LiveSession.class); TableInfoHelper.initTableInfo(assistant, LiveReserve.class); TableInfoHelper.initTableInfo(assistant, Expert.class); } @Mock private LiveCourseMapper liveCourseMapper; @Mock private LiveSessionMapper liveSessionMapper; @Mock private LiveReserveMapper liveReserveMapper; @Mock private ExpertMapper expertMapper; @Mock private AppAuthSessionService appAuthSessionService; private AppLiveCourseService appLiveCourseService; @Captor private ArgumentCaptor liveReserveCaptor; /** * 每个用例前重建被测服务。 */ @BeforeEach void setUp() { AppLiveExpertService appLiveExpertService = new AppLiveExpertService(expertMapper); appLiveCourseService = new AppLiveCourseService( liveCourseMapper, liveSessionMapper, liveReserveMapper, expertMapper, appAuthSessionService, new AppLiveCourseDisplaySupport(), appLiveExpertService ); } /** * 未开始场次且用户已预约时,应回显预约态详情。 */ @Test void detailShouldReturnReservedActionWhenUpcomingSessionReserved() { LiveCourse course = buildCourse(10L, 8L); LiveSession nextSession = buildSession(101L, 10L, LocalDateTime.now().plusDays(1), AppLiveCourseDisplaySupport.SESSION_STATUS_NOT_STARTED); nextSession.setBaseReserveCount(10); nextSession.setRealReserveCount(2); nextSession.setBaseWatchCount(3); nextSession.setRealWatchCount(4); when(liveCourseMapper.selectOne(any())).thenReturn(course); when(liveSessionMapper.selectList(any())).thenReturn(Collections.singletonList(nextSession)); when(expertMapper.selectOne(any())).thenReturn(buildExpert(8L)); when(appAuthSessionService.isLogin()).thenReturn(true); when(appAuthSessionService.getLoginUserId()).thenReturn(66L); when(liveReserveMapper.selectCount(any())).thenReturn(1L); AppLiveCourseDetailVO detailVO = appLiveCourseService.detail(10L); assertEquals("NOT_STARTED", detailVO.getDisplayStatus()); assertEquals("RESERVED", detailVO.getActionStatus()); assertEquals("已预约", detailVO.getActionText()); assertEquals(Long.valueOf(101L), detailVO.getActionSessionId()); assertNotNull(detailVO.getCountdownTime()); assertEquals(Long.valueOf(12L), detailVO.getReserveCount()); assertEquals(Long.valueOf(7L), detailVO.getWatchCount()); assertEquals("张雨泽", detailVO.getExpert().getName()); } /** * 直播结束后五分钟内仍应保持立即查看按钮。 */ @Test void detailShouldKeepWatchableDuringEndedCooldown() { LiveCourse course = buildCourse(20L, 9L); LiveSession endedSession = buildSession(201L, 20L, LocalDateTime.now().minusHours(1), AppLiveCourseDisplaySupport.SESSION_STATUS_ENDED); endedSession.setActualEndTime(LocalDateTime.now().minusMinutes(3)); endedSession.setVideoUrl("https://cdn.example/live.m3u8"); LiveSession nextSession = buildSession(202L, 20L, LocalDateTime.now().plusDays(2), AppLiveCourseDisplaySupport.SESSION_STATUS_NOT_STARTED); when(liveCourseMapper.selectOne(any())).thenReturn(course); when(liveSessionMapper.selectList(any())).thenReturn(Arrays.asList(endedSession, nextSession)); when(expertMapper.selectOne(any())).thenReturn(buildExpert(9L)); when(appAuthSessionService.isLogin()).thenReturn(false); when(liveReserveMapper.selectCount(any())).thenReturn(0L); AppLiveCourseDetailVO detailVO = appLiveCourseService.detail(20L); assertEquals("ENDED", detailVO.getDisplayStatus()); assertEquals("WATCHABLE", detailVO.getActionStatus()); assertEquals("立即查看", detailVO.getActionText()); assertEquals(Long.valueOf(201L), detailVO.getActionSessionId()); assertNull(detailVO.getCountdownTime()); assertEquals("https://cdn.example/live.m3u8", detailVO.getVideoUrl()); } /** * 历史残留的直播中场次不应覆盖时间线上最近一场已结束场次的展示态。 */ @Test void detailShouldIgnoreHistoricalLivingSessionWhenLatestStartedSessionEnded() { LiveCourse course = buildCourse(21L, 9L); LiveSession staleLivingSession = buildSession(210L, 21L, LocalDateTime.now().minusDays(1), AppLiveCourseDisplaySupport.SESSION_STATUS_LIVING); staleLivingSession.setVideoUrl("https://cdn.example/stale.m3u8"); LiveSession latestEndedSession = buildSession(211L, 21L, LocalDateTime.now().minusHours(1), AppLiveCourseDisplaySupport.SESSION_STATUS_ENDED); latestEndedSession.setActualEndTime(LocalDateTime.now().minusMinutes(2)); latestEndedSession.setVideoUrl("https://cdn.example/latest-ended.m3u8"); when(liveCourseMapper.selectOne(any())).thenReturn(course); when(liveSessionMapper.selectList(any())).thenReturn(Arrays.asList(staleLivingSession, latestEndedSession)); when(expertMapper.selectOne(any())).thenReturn(buildExpert(9L)); when(appAuthSessionService.isLogin()).thenReturn(false); when(liveReserveMapper.selectCount(any())).thenReturn(0L); AppLiveCourseDetailVO detailVO = appLiveCourseService.detail(21L); assertEquals("ENDED", detailVO.getDisplayStatus()); assertEquals("WATCHABLE", detailVO.getActionStatus()); assertEquals(Long.valueOf(211L), detailVO.getActionSessionId()); assertEquals("https://cdn.example/latest-ended.m3u8", detailVO.getVideoUrl()); } /** * 首次预约时应写入直播预约记录。 */ @Test void reserveShouldInsertRecordWhenNoActiveRecordExists() { LiveCourse course = buildCourse(30L, 10L); LiveSession nextSession = buildSession(301L, 30L, LocalDateTime.now().plusHours(3), AppLiveCourseDisplaySupport.SESSION_STATUS_NOT_STARTED); when(appAuthSessionService.requireLoginUserId()).thenReturn(88L); when(liveCourseMapper.selectOne(any())).thenReturn(course); when(liveSessionMapper.selectOne(any())).thenReturn(nextSession); when(liveReserveMapper.selectOne(any())).thenReturn(null); when(liveSessionMapper.selectList(any())).thenReturn(Collections.singletonList(nextSession)); when(liveReserveMapper.selectCount(any())).thenReturn(1L); AppLiveReserveResultVO resultVO = appLiveCourseService.reserve(30L, 301L); verify(liveReserveMapper).insert(liveReserveCaptor.capture()); assertEquals(Long.valueOf(30L), liveReserveCaptor.getValue().getLiveCourseId()); assertEquals(Long.valueOf(301L), liveReserveCaptor.getValue().getSessionId()); assertEquals(Long.valueOf(88L), liveReserveCaptor.getValue().getUserId()); assertEquals(Integer.valueOf(0), liveReserveCaptor.getValue().getIsCancel()); assertEquals(Boolean.TRUE, resultVO.getReserved()); assertEquals(Long.valueOf(1L), resultVO.getReserveCount()); } /** * 已取消的预约记录再次预约时应恢复有效状态。 */ @Test void reserveShouldRestoreCancelledRecordWhenExists() { LiveCourse course = buildCourse(40L, 11L); LiveSession nextSession = buildSession(401L, 40L, LocalDateTime.now().plusHours(5), AppLiveCourseDisplaySupport.SESSION_STATUS_NOT_STARTED); LiveReserve cancelledReserve = new LiveReserve(); cancelledReserve.setId(501L); cancelledReserve.setLiveCourseId(40L); cancelledReserve.setSessionId(401L); cancelledReserve.setUserId(99L); cancelledReserve.setIsCancel(1); when(appAuthSessionService.requireLoginUserId()).thenReturn(99L); when(liveCourseMapper.selectOne(any())).thenReturn(course); when(liveSessionMapper.selectOne(any())).thenReturn(nextSession); when(liveReserveMapper.selectOne(any())).thenReturn(cancelledReserve); when(liveSessionMapper.selectList(any())).thenReturn(Collections.singletonList(nextSession)); when(liveReserveMapper.selectCount(any())).thenReturn(2L); AppLiveReserveResultVO resultVO = appLiveCourseService.reserve(40L, 401L); verify(liveReserveMapper).updateById(liveReserveCaptor.capture()); assertEquals(Integer.valueOf(0), liveReserveCaptor.getValue().getIsCancel()); assertNotNull(liveReserveCaptor.getValue().getReserveTime()); assertEquals(Long.valueOf(2L), resultVO.getReserveCount()); } /** * 构造启用中的直播课程测试数据。 */ private LiveCourse buildCourse(Long courseId, Long expertId) { LiveCourse course = new LiveCourse(); course.setId(courseId); course.setExpertId(expertId); course.setTitle("中医营养师直播课"); course.setCoverImg("https://cdn.example/cover.png"); course.setIntro("课程详情"); course.setTargetUsers("适用人群"); course.setQuizPoints(3); course.setStatus(1); return course; } /** * 构造直播场次测试数据。 */ private LiveSession buildSession(Long sessionId, Long courseId, LocalDateTime startTime, Integer liveStatus) { LiveSession session = new LiveSession(); session.setId(sessionId); session.setLiveCourseId(courseId); session.setStartTime(startTime); session.setLiveStatus(liveStatus); return session; } /** * 构造启用中的专家测试数据。 */ private Expert buildExpert(Long expertId) { Expert expert = new Expert(); expert.setId(expertId); expert.setName("张雨泽"); expert.setAvatar("https://cdn.example/avatar.png"); expert.setSpecialty("营养调理"); expert.setIntro("专家介绍"); expert.setStatus(1); return expert; } }