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.dto.AppLiveQuizAnswerRequest; import com.tcm.training.admin.app.live.dto.AppLiveQuizSubmitRequest; 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.AppLiveQuizSubmitResultVO; 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.entity.LiveSessionQuestion; import com.tcm.training.admin.live.entity.LiveSessionQuizRecord; 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 com.tcm.training.admin.live.mapper.LiveSessionQuestionMapper; import com.tcm.training.admin.live.mapper.LiveSessionQuizRecordMapper; import com.tcm.training.admin.live.mapper.LiveWatchRecordMapper; import com.tcm.training.admin.points.mapper.UserPointsAccountMapper; import com.tcm.training.admin.points.service.PointsLedgerAppService; 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 java.util.List; 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.ArgumentMatchers.anyLong; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; 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 LiveSessionQuestionMapper liveSessionQuestionMapper; @Mock private LiveSessionQuizRecordMapper liveSessionQuizRecordMapper; @Mock private LiveWatchRecordMapper liveWatchRecordMapper; @Mock private UserPointsAccountMapper userPointsAccountMapper; @Mock private PointsLedgerAppService pointsLedgerAppService; @Mock private AppAuthSessionService appAuthSessionService; private AppLiveCourseService appLiveCourseService; @Captor private ArgumentCaptor liveReserveCaptor; /** * 每个用例前重建被测服务。 */ @BeforeEach void setUp() { AppLiveExpertService appLiveExpertService = new AppLiveExpertService(expertMapper); // 详情与预约用例不关心答题和观看明细,统一返回空列表避免空指针干扰主断言。 lenient().when(liveSessionQuestionMapper.selectList(any())).thenReturn(Collections.emptyList()); lenient().when(liveSessionQuizRecordMapper.selectList(any())).thenReturn(Collections.emptyList()); lenient().when(liveWatchRecordMapper.selectList(any())).thenReturn(Collections.emptyList()); appLiveCourseService = new AppLiveCourseService( liveCourseMapper, liveSessionMapper, liveReserveMapper, expertMapper, liveSessionQuestionMapper, liveSessionQuizRecordMapper, liveWatchRecordMapper, userPointsAccountMapper, pointsLedgerAppService, 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()); assertNull(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()); assertNull(detailVO.getVideoUrl()); } /** * 同一直播课同一答题自然日已经获得过积分时,后续场次答对也不重复发放。 */ @Test void submitQuizShouldNotAwardPointsTwiceForSameCourseSameDay() { LiveCourse course = buildCourse(50L, 12L); LiveSession session = buildSession(501L, 50L, LocalDateTime.now().minusMinutes(30), AppLiveCourseDisplaySupport.SESSION_STATUS_LIVING); List questions = Arrays.asList( buildQuestion(1L, 501L, 1, "A"), buildQuestion(2L, 501L, 2, "B"), buildQuestion(3L, 501L, 3, "C") ); when(appAuthSessionService.requireLoginUserId()).thenReturn(77L); when(liveCourseMapper.selectOne(any())).thenReturn(course); when(liveSessionMapper.selectOne(any())).thenReturn(session); when(liveSessionQuestionMapper.selectList(any())).thenReturn(questions); when(liveSessionQuizRecordMapper.selectList(any())).thenReturn(Collections.emptyList()); when(liveSessionQuizRecordMapper.countAwardedCourseQuizPoints(anyLong(), anyLong(), any(), any())).thenReturn(1); AppLiveQuizSubmitResultVO resultVO = appLiveCourseService.submitQuiz(50L, 501L, buildQuizSubmitRequest()); verify(liveSessionQuizRecordMapper, never()).updateById(any(LiveSessionQuizRecord.class)); verify(userPointsAccountMapper, never()).update(any(), any()); verify(pointsLedgerAppService, never()).syncLedger(); assertEquals(Boolean.TRUE, resultVO.getSubmitted()); assertEquals(Boolean.TRUE, resultVO.getAllCorrect()); assertEquals(Boolean.FALSE, resultVO.getPointsAwarded()); assertEquals("今日该课程积分已获得", resultVO.getMessage()); } /** * 首次预约时应写入直播预约记录。 */ @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 LiveSessionQuestion buildQuestion(Long questionId, Long sessionId, Integer sort, String correctOption) { LiveSessionQuestion question = new LiveSessionQuestion(); question.setId(questionId); question.setSessionId(sessionId); question.setSort(sort); question.setStem("题目" + sort); question.setOptionA("选项A"); question.setOptionB("选项B"); question.setOptionC("选项C"); question.setCorrectOption(correctOption); return question; } /** * 构造三题全对的答题提交请求。 */ private AppLiveQuizSubmitRequest buildQuizSubmitRequest() { AppLiveQuizSubmitRequest request = new AppLiveQuizSubmitRequest(); request.setAnswers(Arrays.asList( buildQuizAnswer(1L, "A"), buildQuizAnswer(2L, "B"), buildQuizAnswer(3L, "C") )); return request; } /** * 构造单题答题请求。 */ private AppLiveQuizAnswerRequest buildQuizAnswer(Long questionId, String chosenOption) { AppLiveQuizAnswerRequest answer = new AppLiveQuizAnswerRequest(); answer.setQuestionId(questionId); answer.setChosenOption(chosenOption); return answer; } /** * 构造启用中的专家测试数据。 */ 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; } }