package com.tcm.training.admin.app.course.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tcm.training.admin.app.course.vo.AppHealthCourseDetailVO; import com.tcm.training.admin.app.course.vo.AppHealthCourseExpertVO; import com.tcm.training.admin.app.course.vo.AppHealthCourseVideoVO; import com.tcm.training.admin.live.entity.Expert; import com.tcm.training.admin.live.entity.HealthCourseVideo; import com.tcm.training.admin.live.mapper.ExpertMapper; import com.tcm.training.admin.live.mapper.HealthCourseVideoMapper; import com.tcm.training.admin.live.service.HealthCourseAppService; import com.tcm.training.admin.live.vo.HealthCourseDetailVO; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * APP 养生课程详情聚合服务。 */ @Service public class AppHealthCourseService { private static final int COURSE_STATUS_ENABLED = 1; private static final int VIDEO_STATUS_ENABLED = 1; private final HealthCourseAppService healthCourseAppService; private final HealthCourseVideoMapper healthCourseVideoMapper; private final ExpertMapper expertMapper; /** * 注入 APP 养生课程详情所需依赖。 */ public AppHealthCourseService(HealthCourseAppService healthCourseAppService, HealthCourseVideoMapper healthCourseVideoMapper, ExpertMapper expertMapper) { this.healthCourseAppService = healthCourseAppService; this.healthCourseVideoMapper = healthCourseVideoMapper; this.expertMapper = expertMapper; } /** * 获取 APP 养生课程详情页数据。 */ public AppHealthCourseDetailVO detail(Long courseId) { HealthCourseDetailVO courseDetail = healthCourseAppService.detail(courseId); if (courseDetail.getStatus() == null || courseDetail.getStatus().intValue() != COURSE_STATUS_ENABLED) { throw new IllegalArgumentException("养生课程不存在"); } List enabledVideos = listEnabledVideos(courseId); AppHealthCourseDetailVO detailVO = new AppHealthCourseDetailVO(); detailVO.setId(courseDetail.getId()); detailVO.setTitle(trimToEmpty(courseDetail.getTitle())); detailVO.setBannerImages(resolveBannerImages(courseDetail)); detailVO.setLevelName(trimToEmpty(courseDetail.getLevelName())); detailVO.setDifficultyName(trimToEmpty(courseDetail.getDifficultyName())); detailVO.setSalePrice(defaultPrice(courseDetail.getSalePrice())); detailVO.setLearnUserCount(defaultNumber(courseDetail.getLearnUserCount())); detailVO.setSummary(trimToEmpty(courseDetail.getSummary())); detailVO.setTargetUsers(trimToEmpty(courseDetail.getTargetUsers())); detailVO.setContent(trimToEmpty(courseDetail.getContent())); detailVO.setVideoCount(Integer.valueOf(enabledVideos.size())); detailVO.setTotalDurationSeconds(resolveTotalDurationSeconds(courseDetail, enabledVideos)); detailVO.setTotalDurationText(formatCourseDuration(detailVO.getTotalDurationSeconds())); detailVO.setExpert(resolveExpert(courseDetail.getExpert() == null ? null : courseDetail.getExpert().getId())); List videoVOList = toVideoVOList(enabledVideos); detailVO.setVideos(videoVOList); detailVO.setFreeVideoCount(countFreeVideos(videoVOList)); detailVO.setPaidVideoCount(countPaidVideos(videoVOList)); return detailVO; } /** * 查询课程下启用的视频章节。 */ private List listEnabledVideos(Long courseId) { List records = healthCourseVideoMapper.selectList(new LambdaQueryWrapper() .eq(HealthCourseVideo::getCourseId, courseId) .eq(HealthCourseVideo::getDeleted, 0) .eq(HealthCourseVideo::getStatus, VIDEO_STATUS_ENABLED) .orderByAsc(HealthCourseVideo::getSort) .orderByAsc(HealthCourseVideo::getId)); return records == null ? Collections.emptyList() : records; } /** * 组装课程详情页需要展示的轮播图列表。 */ private List resolveBannerImages(HealthCourseDetailVO courseDetail) { List bannerImages = new ArrayList(); if (courseDetail.getBannerImgs() != null) { for (String bannerImage : courseDetail.getBannerImgs()) { String normalizedImage = trimToEmpty(bannerImage); if (!normalizedImage.isEmpty()) { bannerImages.add(normalizedImage); } } } // 课程未配置轮播图时,回退到封面图,避免前端轮播组件出现空白页。 if (bannerImages.isEmpty()) { String coverImage = trimToEmpty(courseDetail.getCoverImg()); if (!coverImage.isEmpty()) { bannerImages.add(coverImage); } } return bannerImages; } /** * 组装课程详情页专家卡片信息。 */ private AppHealthCourseExpertVO resolveExpert(Long expertId) { if (expertId == null) { return null; } Expert expert = expertMapper.selectOne(new LambdaQueryWrapper() .eq(Expert::getId, expertId) .eq(Expert::getDeleted, 0) .eq(Expert::getStatus, COURSE_STATUS_ENABLED) .last("LIMIT 1")); if (expert == null) { return null; } AppHealthCourseExpertVO expertVO = new AppHealthCourseExpertVO(); expertVO.setId(expert.getId()); expertVO.setName(trimToEmpty(expert.getName())); expertVO.setAvatar(trimToEmpty(expert.getAvatar())); expertVO.setSpecialty(trimToEmpty(expert.getSpecialty())); return expertVO; } /** * 将启用视频列表转换为 APP 详情页可直接渲染的数据结构。 */ private List toVideoVOList(List videos) { if (videos == null || videos.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList(videos.size()); for (HealthCourseVideo video : videos) { AppHealthCourseVideoVO videoVO = new AppHealthCourseVideoVO(); videoVO.setId(video.getId()); videoVO.setChapterName(trimToEmpty(video.getChapterName())); videoVO.setDurationSeconds(defaultNumber(video.getDurationSeconds())); videoVO.setDurationText(formatVideoDuration(videoVO.getDurationSeconds())); videoVO.setSalePrice(defaultPrice(video.getSalePrice())); // 前端课程列表直接按该字段切分免费/付费列表,避免再次复制业务判断。 videoVO.setFreeAccess(Boolean.valueOf(isFreeVideo(videoVO.getSalePrice()))); result.add(videoVO); } return result; } /** * 统计免费章节数量。 */ private Integer countFreeVideos(List videos) { int count = 0; for (AppHealthCourseVideoVO video : videos) { if (Boolean.TRUE.equals(video.getFreeAccess())) { count++; } } return Integer.valueOf(count); } /** * 统计付费章节数量。 */ private Integer countPaidVideos(List videos) { int count = 0; for (AppHealthCourseVideoVO video : videos) { if (!Boolean.TRUE.equals(video.getFreeAccess())) { count++; } } return Integer.valueOf(count); } /** * 解析课程总时长,优先复用课程表已汇总的时长字段。 */ private Integer resolveTotalDurationSeconds(HealthCourseDetailVO courseDetail, List videos) { Integer totalDurationSeconds = courseDetail.getTotalDurationSeconds(); if (totalDurationSeconds != null && totalDurationSeconds.intValue() > 0) { return totalDurationSeconds; } int sum = 0; for (HealthCourseVideo video : videos) { sum += defaultNumber(video.getDurationSeconds()); } return Integer.valueOf(sum); } /** * 判断视频是否属于免费章节。 */ private boolean isFreeVideo(BigDecimal salePrice) { return salePrice == null || salePrice.compareTo(BigDecimal.ZERO) <= 0; } /** * 兜底空价格字段,避免前端价格区展示异常。 */ private BigDecimal defaultPrice(BigDecimal price) { return price == null ? BigDecimal.ZERO : price; } /** * 兜底空数字字段。 */ private Integer defaultNumber(Integer value) { return value == null ? 0 : value; } /** * 将总时长秒数格式化为适合页头展示的简要文案。 */ private String formatCourseDuration(Integer durationSeconds) { int totalSeconds = defaultNumber(durationSeconds); if (totalSeconds <= 0) { return "0分钟"; } int hours = totalSeconds / 3600; int minutes = (totalSeconds % 3600) / 60; if (hours > 0 && minutes > 0) { return hours + "小时" + minutes + "分钟"; } if (hours > 0) { return hours + "小时"; } if (minutes > 0) { return minutes + "分钟"; } return totalSeconds + "秒"; } /** * 将章节时长格式化为 `HH:MM:SS`,便于与当前 APP 设计稿对齐。 */ private String formatVideoDuration(Integer durationSeconds) { int totalSeconds = defaultNumber(durationSeconds); int hours = totalSeconds / 3600; int minutes = (totalSeconds % 3600) / 60; int seconds = totalSeconds % 60; return String.format("%02d:%02d:%02d", hours, minutes, seconds); } /** * 将可空文本转换为空字符串。 */ private String trimToEmpty(String value) { if (value == null) { return ""; } String trimmed = value.trim(); return trimmed.isEmpty() ? "" : trimmed; } }