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