From fb33ef7ea01af3e59178b8437f3ecf8d76b2b32c Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期三, 23 九月 2026 14:52:45 +0800
Subject: [PATCH] fix: 修复直播时间精度问题并更新APP与H5配置
---
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/live/service/LiveSessionAppServiceTest.java | 99 ++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 88 insertions(+), 11 deletions(-)
diff --git a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/live/service/LiveSessionAppServiceTest.java b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/live/service/LiveSessionAppServiceTest.java
index dec15c5..0f2dc2e 100644
--- a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/live/service/LiveSessionAppServiceTest.java
+++ b/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;
@@ -230,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())
@@ -258,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) {
--
Gitblit v1.7.1