“mitao”
2026-04-23 bae38167a14707a0378590b60e09dc8a6bcf2433
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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;
    @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.<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;
    }
}