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<HealthCourseVideo> 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<AppHealthCourseVideoVO> videoVOList = toVideoVOList(enabledVideos);
|
detailVO.setVideos(videoVOList);
|
detailVO.setFreeVideoCount(countFreeVideos(videoVOList));
|
detailVO.setPaidVideoCount(countPaidVideos(videoVOList));
|
return detailVO;
|
}
|
|
/**
|
* 查询课程下启用的视频章节。
|
*/
|
private List<HealthCourseVideo> listEnabledVideos(Long courseId) {
|
List<HealthCourseVideo> records = healthCourseVideoMapper.selectList(new LambdaQueryWrapper<HealthCourseVideo>()
|
.eq(HealthCourseVideo::getCourseId, courseId)
|
.eq(HealthCourseVideo::getDeleted, 0)
|
.eq(HealthCourseVideo::getStatus, VIDEO_STATUS_ENABLED)
|
.orderByAsc(HealthCourseVideo::getSort)
|
.orderByAsc(HealthCourseVideo::getId));
|
return records == null ? Collections.<HealthCourseVideo>emptyList() : records;
|
}
|
|
/**
|
* 组装课程详情页需要展示的轮播图列表。
|
*/
|
private List<String> resolveBannerImages(HealthCourseDetailVO courseDetail) {
|
List<String> bannerImages = new ArrayList<String>();
|
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<Expert>()
|
.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<AppHealthCourseVideoVO> toVideoVOList(List<HealthCourseVideo> videos) {
|
if (videos == null || videos.isEmpty()) {
|
return Collections.emptyList();
|
}
|
List<AppHealthCourseVideoVO> result = new ArrayList<AppHealthCourseVideoVO>(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<AppHealthCourseVideoVO> videos) {
|
int count = 0;
|
for (AppHealthCourseVideoVO video : videos) {
|
if (Boolean.TRUE.equals(video.getFreeAccess())) {
|
count++;
|
}
|
}
|
return Integer.valueOf(count);
|
}
|
|
/**
|
* 统计付费章节数量。
|
*/
|
private Integer countPaidVideos(List<AppHealthCourseVideoVO> 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<HealthCourseVideo> 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;
|
}
|
}
|