hejianhao
2026-09-01 540ef65220e05b75c183cd3554aaeb554927600a
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/live/service/AppLiveCourseService.java
@@ -34,6 +34,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
@@ -138,7 +139,7 @@
        detailVO.setDisplayStatusText(resolveDisplayStatusText(snapshot));
        detailVO.setCountdownTime(resolveCountdownTime(snapshot));
        detailVO.setActionSessionId(actionSession == null ? null : actionSession.getId());
        detailVO.setVideoUrl(actionSession == null ? null : appLiveCourseDisplaySupport.trimToNull(actionSession.getVideoUrl()));
        detailVO.setVideoUrl(resolvePlayableVideoUrl(snapshot, actionSession));
        detailVO.setPlaybackStartTime(playbackStartTime);
        detailVO.setDurationSeconds(actionSession == null ? null : actionSession.getDurationSeconds());
        detailVO.setReserveCount(resolveReserveCount(courseId, sessions));
@@ -206,6 +207,7 @@
        Map<Long, String> normalizedAnswers = validateAndNormalizeAnswers(request.getAnswers(), questions);
        WatchProgressSummary watchSummary = resolveWatchProgress(sessionId, userId, session);
        LocalDateTime answeredAt = LocalDateTime.now();
        boolean allCorrect = true;
        List<LiveSessionQuizRecord> insertedRecords = new ArrayList<LiveSessionQuizRecord>();
        for (LiveSessionQuestion question : questions) {
@@ -222,14 +224,17 @@
            record.setChosenOption(chosenOption);
            record.setIsCorrect(correct ? 1 : 0);
            record.setPointsEarned(0);
            record.setAnsweredAt(LocalDateTime.now());
            record.setAnsweredAt(answeredAt);
            liveSessionQuizRecordMapper.insert(record);
            insertedRecords.add(record);
        }
        boolean pointsAwarded = false;
        if (allCorrect && watchSummary.isWatchCompleted()) {
            pointsAwarded = awardQuizPoints(course, session, userId, insertedRecords, questions);
        boolean dailyCoursePointsAlreadyAwarded = false;
        // 当前业务改为只要三题全部答对就立即发放积分,不再依赖完整观看直播。
        if (allCorrect) {
            dailyCoursePointsAlreadyAwarded = hasAwardedCourseQuizPointsToday(course.getId(), userId, answeredAt);
            pointsAwarded = awardQuizPoints(course, session, userId, insertedRecords, questions, answeredAt);
        }
        AppLiveQuizSubmitResultVO resultVO = new AppLiveQuizSubmitResultVO();
@@ -238,7 +243,7 @@
        resultVO.setWatchCompleted(Boolean.valueOf(watchSummary.isWatchCompleted()));
        resultVO.setPointsAwarded(Boolean.valueOf(pointsAwarded));
        resultVO.setPointsReward(Integer.valueOf(defaultNumber(course.getQuizPoints())));
        resultVO.setMessage(resolveQuizSubmitMessage(allCorrect, watchSummary.isWatchCompleted(), pointsAwarded));
        resultVO.setMessage(resolveQuizSubmitMessage(allCorrect, pointsAwarded, dailyCoursePointsAlreadyAwarded));
        return resultVO;
    }
@@ -250,7 +255,7 @@
                                                              Long sessionId,
                                                              AppLiveWatchRecordUploadRequest request) {
        Long userId = appAuthSessionService.requireLoginUserId();
        LiveCourse course = requireCourse(courseId);
        requireCourse(courseId);
        LiveSession session = requireCourseSession(courseId, sessionId);
        validateWatchRecordRequest(request);
@@ -268,20 +273,15 @@
        record.setIsEffective(defaultNumber(request.getDurationSeconds()) > 0 ? 1 : 0);
        liveWatchRecordMapper.insert(record);
        List<LiveSessionQuestion> questions = listSessionQuestions(sessionId);
        List<LiveSessionQuizRecord> quizRecords = listUserQuizRecords(sessionId, userId);
        QuizUserState quizUserState = resolveQuizUserState(questions, quizRecords, watchCompleted);
        boolean pointsAwarded = quizUserState.isQuizPointsAwarded();
        if (quizUserState.isQuizAllCorrect() && watchCompleted && !quizUserState.isQuizPointsAwarded()) {
            pointsAwarded = awardQuizPoints(course, session, userId, quizRecords, questions);
        }
        QuizUserState quizUserState = resolveQuizUserState(listSessionQuestions(sessionId), quizRecords, watchCompleted);
        AppLiveWatchRecordUploadResultVO resultVO = new AppLiveWatchRecordUploadResultVO();
        resultVO.setCumulativeWatchSeconds(Integer.valueOf(cumulativeWatchSeconds));
        resultVO.setWatchCompleted(Boolean.valueOf(watchCompleted));
        resultVO.setQuizSubmitted(Boolean.valueOf(quizUserState.isQuizSubmitted()));
        resultVO.setQuizAllCorrect(Boolean.valueOf(quizUserState.isQuizAllCorrect()));
        resultVO.setQuizPointsAwarded(Boolean.valueOf(pointsAwarded));
        resultVO.setQuizPointsAwarded(Boolean.valueOf(quizUserState.isQuizPointsAwarded()));
        return resultVO;
    }
@@ -650,9 +650,13 @@
                                    LiveSession session,
                                    Long userId,
                                    List<LiveSessionQuizRecord> quizRecords,
                                    List<LiveSessionQuestion> questions) {
                                    List<LiveSessionQuestion> questions,
                                    LocalDateTime answeredAt) {
        int pointsReward = defaultNumber(course.getQuizPoints());
        if (pointsReward <= 0 || hasAwardedQuizPoints(quizRecords)) {
            return false;
        }
        if (hasAwardedCourseQuizPointsToday(course.getId(), userId, answeredAt)) {
            return false;
        }
        LiveSessionQuizRecord rewardRecord = resolveRewardRecord(quizRecords, questions);
@@ -666,6 +670,23 @@
        // 积分到账后立即同步流水,避免 APP 积分页与直播观看页状态延迟。
        pointsLedgerAppService.syncLedger();
        return true;
    }
    /**
     * 判断用户在同一直播课的答题自然日内是否已获得过积分奖励。
     */
    private boolean hasAwardedCourseQuizPointsToday(Long courseId, Long userId, LocalDateTime answeredAt) {
        if (courseId == null || userId == null) {
            return false;
        }
        LocalDate answerDate = (answeredAt == null ? LocalDateTime.now() : answeredAt).toLocalDate();
        Integer awardedCount = liveSessionQuizRecordMapper.countAwardedCourseQuizPoints(
                courseId,
                userId,
                answerDate.atStartOfDay(),
                answerDate.plusDays(1).atStartOfDay()
        );
        return awardedCount != null && awardedCount.intValue() > 0;
    }
    /**
@@ -726,15 +747,17 @@
    /**
     * 构造答题提交后的提示文案。
     */
    private String resolveQuizSubmitMessage(boolean allCorrect, boolean watchCompleted, boolean pointsAwarded) {
    private String resolveQuizSubmitMessage(boolean allCorrect,
                                            boolean pointsAwarded,
                                            boolean dailyCoursePointsAlreadyAwarded) {
        if (!allCorrect) {
            return "回答错误,本次不获得积分";
        }
        if (pointsAwarded) {
            return "答题成功,积分已到账";
        }
        if (!watchCompleted) {
            return "已全部答对,完整观看直播后自动发放积分";
        if (dailyCoursePointsAlreadyAwarded) {
            return "今日已获得积分,明日再来";
        }
        return "答题成功";
    }
@@ -826,6 +849,17 @@
    }
    /**
     * 解析观看页可播放视频地址,直播结束冷却期只保留答题入口不继续播放视频。
     */
    private String resolvePlayableVideoUrl(AppLiveCourseDisplaySupport.CourseDisplaySnapshot snapshot,
                                           LiveSession actionSession) {
        if (snapshot.isEndedCooldown() || actionSession == null) {
            return null;
        }
        return appLiveCourseDisplaySupport.trimToNull(actionSession.getVideoUrl());
    }
    /**
     * 解析模拟直播回放进度的起播时间,优先使用实际开播时间。
     */
    private LocalDateTime resolvePlaybackStartTime(LiveSession actionSession) {
@@ -850,9 +884,14 @@
     * 判断当前场次是否已达到答题开放时间。
     */
    private boolean isQuizEnabled(LiveSession session, LocalDateTime quizAvailableAt, LocalDateTime now) {
        return session != null
                && quizAvailableAt != null
                && !quizAvailableAt.isAfter(now);
        if (session == null || quizAvailableAt == null || quizAvailableAt.isAfter(now)) {
            return false;
        }
        if (appLiveCourseDisplaySupport.defaultNumber(session.getLiveStatus()) == AppLiveCourseDisplaySupport.SESSION_STATUS_LIVING) {
            return true;
        }
        // 直播结束后的五分钟冷却窗口内继续允许用户进入直播间并完成答题。
        return appLiveCourseDisplaySupport.isInEndedCooldown(session, now, DETAIL_WATCH_KEEP_MINUTES);
    }
    /**