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<LiveSessionQuestion> questionCaptor;
|
|
/**
|
* 每个用例前重建被测服务。
|
*/
|
@BeforeEach
|
void setUp() {
|
liveSessionAppService = new LiveSessionAppService(
|
liveCourseMapper,
|
liveSessionMapper,
|
liveSessionQuestionMapper,
|
liveReserveMapper
|
);
|
}
|
|
/**
|
* 编辑场次题目时应先物理删除旧题,避免逻辑删除记录触发唯一索引冲突。
|
*/
|
@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.<LiveSessionQuestion>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<LiveSession> page =
|
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LiveSession>(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.<LiveSessionQuestion>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;
|
}
|
}
|