zjk
2026-09-29 54dcf0a82e7490fa58b1c056d5c0b9cb739c2931
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/live/service/LiveSessionAppServiceTest.java
@@ -18,6 +18,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -65,6 +67,8 @@
    @Captor
    private ArgumentCaptor<LiveSessionQuestion> questionCaptor;
    @Mock
    private LiveRemindDispatchService liveRemindDispatchService;
    /**
     * 每个用例前重建被测服务。
@@ -75,7 +79,8 @@
                liveCourseMapper,
                liveSessionMapper,
                liveSessionQuestionMapper,
                liveReserveMapper
                liveReserveMapper,
                liveRemindDispatchService
        );
    }
@@ -227,26 +232,36 @@
    }
    /**
     * 同课程时间线已推进到后续场次时,应将历史残留的直播中场次收敛为已结束。
     * 仅后续直播中场次能替换当前直播,已结束的历史记录不能挤掉它。
     */
    @Test
    void reconcileCourseLivingSessionsShouldCloseHistoricalLivingSession() {
    @ParameterizedTest
    @ValueSource(ints = {1, 2})
    void reconcileCourseLivingSessionsShouldOnlyCloseForNewerLivingSession(int latestStatus) {
        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));
        LiveSession latestSession = new LiveSession();
        latestSession.setId(90002L);
        latestSession.setLiveCourseId(9000L);
        latestSession.setLiveStatus(latestStatus);
        latestSession.setStartTime(LocalDateTime.of(2026, 4, 9, 10, 0));
        if (latestStatus == 2) {
            latestSession.setActualEndTime(LocalDateTime.of(2026, 4, 9, 10, 30));
        }
        when(liveSessionMapper.selectList(any())).thenReturn(Arrays.asList(staleLivingSession, latestEndedSession));
        when(liveSessionMapper.selectList(any())).thenReturn(Arrays.asList(staleLivingSession, latestSession));
        assertEquals(1, liveSessionAppService.reconcileCourseLivingSessions(9000L, LocalDateTime.of(2026, 4, 9, 10, 35)));
        int repairedCount = liveSessionAppService.reconcileCourseLivingSessions(
                9000L, LocalDateTime.of(2026, 4, 9, 10, 35));
        if (latestStatus == 2) {
            assertEquals(0, repairedCount);
            verify(liveSessionMapper, never()).updateById(any(LiveSession.class));
            return;
        }
        assertEquals(1, repairedCount);
        verify(liveSessionMapper).updateById(org.mockito.ArgumentMatchers.argThat(session ->
                session != null
                        && Long.valueOf(90001L).equals(session.getId())
@@ -255,6 +270,71 @@
    }
    /**
     * 模拟 DATETIME 按秒四舍五入,覆盖半秒边界及跨秒、跨日进位。
     */
    @ParameterizedTest
    @ValueSource(ints = {0, 100000000, 499999999, 500000000, 800000000, 999999999})
    void autoStartShouldRemainLivingWhenDatabaseRoundsTime(int nanos) {
        LiveSession session = mockPendingSessionWithDatabaseRounding();
        LocalDateTime startTime = LocalDateTime.of(2026, 9, 22, 23, 59, 59, nanos);
        session.setStartTime(startTime.withNano(0));
        LiveSessionStatusVO result = liveSessionAppService.autoStart(session.getId(), startTime);
        assertEquals(Integer.valueOf(1), result.getLiveStatus());
        assertEquals(startTime.withNano(0), result.getActualStartTime());
        assertNull(result.getActualEndTime());
        verify(liveSessionMapper, never()).updateById(any(LiveSession.class));
        verify(liveSessionMapper).markLivingIfPending(session.getId(), startTime.withNano(0));
    }
    /**
     * 人工提前开播也应经过相同的秒级归一化,而不是被未来的计划时间误结束。
     */
    @Test
    void manualStartShouldUseSecondPrecisionBeforePlannedTime() {
        LiveSession session = mockPendingSessionWithDatabaseRounding();
        session.setStartTime(LocalDateTime.now().plusHours(1));
        LiveCourse course = new LiveCourse();
        course.setId(session.getLiveCourseId());
        when(liveCourseMapper.selectById(course.getId())).thenReturn(course);
        LiveSessionStatusVO result = liveSessionAppService.start(course.getId(), session.getId());
        ArgumentCaptor<LocalDateTime> timeCaptor = ArgumentCaptor.forClass(LocalDateTime.class);
        verify(liveSessionMapper).markLivingIfPending(
                org.mockito.ArgumentMatchers.eq(session.getId()), timeCaptor.capture());
        assertEquals(0, timeCaptor.getValue().getNano());
        assertEquals(Integer.valueOf(1), result.getLiveStatus());
        assertNull(result.getActualEndTime());
        verify(liveSessionMapper, never()).updateById(any(LiveSession.class));
    }
    private LiveSession mockPendingSessionWithDatabaseRounding() {
        LiveSession session = new LiveSession();
        session.setId(90008L);
        session.setLiveCourseId(9000L);
        session.setLiveStatus(0);
        session.setDurationSeconds(385);
        when(liveSessionMapper.selectById(session.getId())).thenReturn(session);
        when(liveSessionMapper.selectList(any())).thenReturn(Collections.singletonList(session));
        when(liveSessionMapper.markLivingIfPending(
                org.mockito.ArgumentMatchers.eq(session.getId()), any(LocalDateTime.class)))
                .thenAnswer(invocation -> {
                    LocalDateTime input = invocation.getArgument(1);
                    // 保留与 MySQL 秒精度写入一致的进位模型,不依赖真实数据库。
                    LocalDateTime persisted = input.withNano(0);
                    if (input.getNano() >= 500000000) {
                        persisted = persisted.plusSeconds(1);
                    }
                    session.setActualStartTime(persisted);
                    session.setLiveStatus(1);
                    return 1;
                });
        return session;
    }
    /**
     * 构造题目请求。
     */
    private LiveSessionQuestionRequest buildQuestion(int sort, String stem) {