zjk
2026-09-29 54dcf0a82e7490fa58b1c056d5c0b9cb739c2931
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java
@@ -153,6 +153,7 @@
            itemVO.setDifficultyName(trimToEmpty(row.getDifficultyName()));
            itemVO.setSalePrice(defaultPrice(row.getSalePrice()));
            itemVO.setLearnUserCount(defaultNumber(row.getLearnUserCount()));
            itemVO.setStudyingCount(defaultNumber(row.getStudyingCount()));
            result.add(itemVO);
        }
        return result;
@@ -166,6 +167,8 @@
        if (courseDetail.getStatus() == null || courseDetail.getStatus().intValue() != COURSE_STATUS_ENABLED) {
            throw new IllegalArgumentException("养生课程不存在");
        }
        // 进入课程详情即视为一次学习行为,正在学习人数自增;返回值按自增后的口径展示
        incrementStudyingCount(courseId);
        List<HealthCourseVideo> enabledVideos = listEnabledVideos(courseId);
        AppHealthCourseDetailVO detailVO = new AppHealthCourseDetailVO();
@@ -176,6 +179,7 @@
        detailVO.setDifficultyName(trimToEmpty(courseDetail.getDifficultyName()));
        detailVO.setSalePrice(defaultPrice(courseDetail.getSalePrice()));
        detailVO.setLearnUserCount(defaultNumber(courseDetail.getLearnUserCount()));
        detailVO.setStudyingCount(Integer.valueOf(defaultNumber(courseDetail.getStudyingCount()) + 1));
        detailVO.setSummary(trimToEmpty(courseDetail.getSummary()));
        detailVO.setTargetUsers(trimToEmpty(courseDetail.getTargetUsers()));
        detailVO.setContent(trimToEmpty(courseDetail.getContent()));
@@ -218,6 +222,7 @@
            itemVO.setTeacherName(trimToEmpty(row.getTeacherName()));
            itemVO.setTeacherAvatar(trimToEmpty(row.getTeacherAvatar()));
            itemVO.setLearnUserCount(defaultNumber(row.getLearnUserCount()));
            itemVO.setStudyingCount(defaultNumber(row.getStudyingCount()));
            itemVO.setFavoritedAt(row.getFavoritedAt());
            result.add(itemVO);
        }
@@ -311,17 +316,25 @@
        HealthCourse course = requireEnabledCourse(courseId);
        List<HealthCourseVideo> enabledVideos = listEnabledVideos(courseId);
        Map<Long, VideoProgressSnapshot> videoProgressSnapshotMap = buildVideoProgressSnapshotMap(userId, Collections.singletonList(courseId));
        PurchaseStateSnapshot purchaseStateSnapshot = resolvePurchaseState(courseId);
        List<AppMyHealthCourseVideoItemVO> videos = new ArrayList<AppMyHealthCourseVideoItemVO>(enabledVideos.size());
        Long currentVideoId = null;
        LocalDateTime latestLearnAt = null;
        for (HealthCourseVideo video : enabledVideos) {
            VideoProgressSnapshot snapshot = videoProgressSnapshotMap.get(video.getId());
            boolean freeAccess = isFreeVideo(video.getSalePrice());
            boolean purchased = purchaseStateSnapshot.isCourseOwned()
                    || purchaseStateSnapshot.getPurchasedVideoIds().contains(video.getId());
            boolean locked = !freeAccess && !purchased;
            AppMyHealthCourseVideoItemVO itemVO = new AppMyHealthCourseVideoItemVO();
            itemVO.setVideoId(video.getId());
            itemVO.setChapterName(trimToEmpty(video.getChapterName()));
            itemVO.setDurationSeconds(defaultNumber(video.getDurationSeconds()));
            itemVO.setDurationText(formatVideoDuration(itemVO.getDurationSeconds()));
            itemVO.setFreeAccess(Boolean.valueOf(freeAccess));
            itemVO.setPurchased(Boolean.valueOf(purchased));
            itemVO.setLocked(Boolean.valueOf(locked));
            itemVO.setVideoUrl(trimToEmpty(video.getVideoUrl()));
            itemVO.setCompleted(Boolean.valueOf(snapshot != null && snapshot.isCompleted()));
            itemVO.setProgressPercent(Integer.valueOf(calculateVideoDisplayProgressPercent(
@@ -332,14 +345,20 @@
            itemVO.setLastPlayPositionSeconds(Integer.valueOf(resolveVideoResumePosition(snapshot)));
            itemVO.setDownloadable(Boolean.valueOf(hasVideoDownloadAccess(userId, courseId, video.getId())));
            videos.add(itemVO);
            if (snapshot != null && snapshot.getLatestLearnAt() != null
            // 默认播放章节只允许落在当前用户可观看的小节上,未购小节保留展示但不自动选中。
            if (!locked && snapshot != null && snapshot.getLatestLearnAt() != null
                    && (latestLearnAt == null || snapshot.getLatestLearnAt().isAfter(latestLearnAt))) {
                latestLearnAt = snapshot.getLatestLearnAt();
                currentVideoId = video.getId();
            }
        }
        if (currentVideoId == null && !enabledVideos.isEmpty()) {
            currentVideoId = enabledVideos.get(0).getId();
        if (currentVideoId == null) {
            for (AppMyHealthCourseVideoItemVO itemVO : videos) {
                if (!Boolean.TRUE.equals(itemVO.getLocked())) {
                    currentVideoId = itemVO.getVideoId();
                    break;
                }
            }
        }
        AppMyHealthCourseDetailVO detailVO = new AppMyHealthCourseDetailVO();
@@ -508,9 +527,6 @@
        AppUser user = requireEnabledUser(userId);
        HealthCourse course = requireEnabledCourse(courseId);
        String paymentMethod = normalizePaymentMethod(request == null ? null : request.getPaymentMethod());
        if (course.getSalePrice() == null || course.getSalePrice().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("当前课程无需购买");
        }
        CoursePurchaseOrder existingOrder = findLatestActivePaidCourseOrder(userId, courseId);
        if (existingOrder != null) {
            return buildPurchaseResult(existingOrder, true, resolvePurchasedVideoIds(userId, courseId, true));
@@ -994,6 +1010,8 @@
            videoVO.setLocked(Boolean.valueOf(locked));
            // 已购买章节需要直接拿到视频地址,前端才能切换为下载入口。
            videoVO.setVideoUrl(trimToEmpty(video.getVideoUrl()));
            videoVO.setCreatedAt(video.getCreatedAt());
            videoVO.setUpdatedAt(video.getUpdatedAt());
            result.add(videoVO);
        }
        return result;
@@ -1397,6 +1415,16 @@
    }
    /**
     * 正在学习人数自增 1,进入课程详情时调用。
     */
    private void incrementStudyingCount(Long courseId) {
        LambdaUpdateWrapper<HealthCourse> updateWrapper = new LambdaUpdateWrapper<HealthCourse>()
                .eq(HealthCourse::getId, courseId)
                .setSql("studying_count = COALESCE(studying_count, 0) + 1");
        healthCourseMapper.update(null, updateWrapper);
    }
    /**
     * 构造收藏操作返回结果,避免控制器层重复拼装。
     */
    private AppHealthCourseFavoriteResultVO buildFavoriteResult(Long courseId, boolean favorited, Integer favoriteCount) {