package com.tcm.training.admin.app.course.service;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
|
import com.tcm.training.admin.app.course.dto.AppHealthCourseDownloadBatchDeleteRequest;
|
import com.tcm.training.admin.app.course.dto.AppHealthCourseDownloadReportRequest;
|
import com.tcm.training.admin.app.course.dto.AppHealthCourseFavoriteBatchCancelRequest;
|
import com.tcm.training.admin.app.course.dto.AppHealthCoursePlayRecordUploadRequest;
|
import com.tcm.training.admin.app.course.dto.AppHealthCoursePurchaseRequest;
|
import com.tcm.training.admin.app.course.vo.AppHealthCourseCategoryOptionVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCourseDetailVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCourseDownloadItemVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCourseExpertVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCourseFavoriteItemVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCourseFavoriteResultVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCourseListItemVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCoursePlayRecordUploadResultVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCoursePurchaseResultVO;
|
import com.tcm.training.admin.app.course.vo.AppHealthCourseVideoVO;
|
import com.tcm.training.admin.app.course.vo.AppMyHealthCourseDetailVO;
|
import com.tcm.training.admin.app.course.vo.AppMyHealthCourseItemVO;
|
import com.tcm.training.admin.app.course.vo.AppMyHealthCourseVideoItemVO;
|
import com.tcm.training.admin.live.entity.CoursePurchaseOrder;
|
import com.tcm.training.admin.live.entity.Expert;
|
import com.tcm.training.admin.live.entity.HealthCourse;
|
import com.tcm.training.admin.live.entity.HealthCourseDownloadRecord;
|
import com.tcm.training.admin.live.entity.HealthCourseFavoriteRecord;
|
import com.tcm.training.admin.live.entity.HealthCourseMemberJoinRecord;
|
import com.tcm.training.admin.live.entity.HealthCoursePlayRecord;
|
import com.tcm.training.admin.live.entity.HealthCourseVideo;
|
import com.tcm.training.admin.live.entity.LiveCourseCategory;
|
import com.tcm.training.admin.live.mapper.CoursePurchaseOrderMapper;
|
import com.tcm.training.admin.live.mapper.ExpertMapper;
|
import com.tcm.training.admin.live.mapper.HealthCourseDownloadRecordMapper;
|
import com.tcm.training.admin.live.mapper.HealthCourseFavoriteRecordMapper;
|
import com.tcm.training.admin.live.mapper.HealthCourseMapper;
|
import com.tcm.training.admin.live.mapper.HealthCourseMemberJoinRecordMapper;
|
import com.tcm.training.admin.live.mapper.HealthCoursePlayRecordMapper;
|
import com.tcm.training.admin.live.mapper.HealthCourseVideoMapper;
|
import com.tcm.training.admin.live.mapper.LiveCourseCategoryMapper;
|
import com.tcm.training.admin.live.row.AppHealthCourseDownloadRow;
|
import com.tcm.training.admin.live.row.AppHealthCourseFavoriteRow;
|
import com.tcm.training.admin.live.row.AppMyHealthCourseProgressRow;
|
import com.tcm.training.admin.live.row.AppMyHealthCourseVideoProgressRow;
|
import com.tcm.training.admin.live.service.HealthCourseAppService;
|
import com.tcm.training.admin.live.service.LiveCourseCategoryAppService;
|
import com.tcm.training.admin.live.vo.HealthCourseDetailVO;
|
import com.tcm.training.admin.live.vo.HealthCourseListRow;
|
import com.tcm.training.admin.live.vo.CoursePurchaseOrderListRow;
|
import com.tcm.training.admin.member.entity.AppUser;
|
import com.tcm.training.admin.member.mapper.UserMapper;
|
import com.tcm.training.admin.system.service.SystemSettingsAppService;
|
import com.tcm.training.admin.system.vo.SystemSettingsVO;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDateTime;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.Comparator;
|
import java.util.HashMap;
|
import java.util.LinkedHashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import java.time.format.DateTimeFormatter;
|
|
/**
|
* APP 养生课程详情聚合服务。
|
*/
|
@Service
|
public class AppHealthCourseService {
|
|
private static final int COURSE_STATUS_ENABLED = 1;
|
private static final int VIDEO_STATUS_ENABLED = 1;
|
private static final String ACTION_TYPE_FAVORITE = "FAVORITE";
|
private static final String ACTION_TYPE_CANCEL = "CANCEL";
|
private static final String COURSE_TYPE_HEALTH = "HEALTH_COURSE";
|
private static final String PURCHASE_TARGET_COURSE = "COURSE";
|
private static final String PURCHASE_TARGET_CHAPTER = "COURSE_CHAPTER";
|
private static final String PAYMENT_METHOD_WECHAT = "WECHAT";
|
private static final String PAYMENT_METHOD_ALIPAY = "ALIPAY";
|
private static final String ORDER_STATUS_PAID = "PAID";
|
private static final DateTimeFormatter ORDER_NO_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
|
private final HealthCourseAppService healthCourseAppService;
|
private final HealthCourseMapper healthCourseMapper;
|
private final HealthCourseVideoMapper healthCourseVideoMapper;
|
private final LiveCourseCategoryMapper liveCourseCategoryMapper;
|
private final ExpertMapper expertMapper;
|
private final HealthCourseDownloadRecordMapper healthCourseDownloadRecordMapper;
|
private final HealthCourseFavoriteRecordMapper healthCourseFavoriteRecordMapper;
|
private final HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper;
|
private final HealthCoursePlayRecordMapper healthCoursePlayRecordMapper;
|
private final AppAuthSessionService appAuthSessionService;
|
private final CoursePurchaseOrderMapper coursePurchaseOrderMapper;
|
private final UserMapper userMapper;
|
private final SystemSettingsAppService systemSettingsAppService;
|
|
/**
|
* 注入 APP 养生课程详情所需依赖。
|
*/
|
public AppHealthCourseService(HealthCourseAppService healthCourseAppService,
|
HealthCourseMapper healthCourseMapper,
|
HealthCourseVideoMapper healthCourseVideoMapper,
|
LiveCourseCategoryMapper liveCourseCategoryMapper,
|
ExpertMapper expertMapper,
|
HealthCourseDownloadRecordMapper healthCourseDownloadRecordMapper,
|
HealthCourseFavoriteRecordMapper healthCourseFavoriteRecordMapper,
|
HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper,
|
HealthCoursePlayRecordMapper healthCoursePlayRecordMapper,
|
AppAuthSessionService appAuthSessionService,
|
CoursePurchaseOrderMapper coursePurchaseOrderMapper,
|
UserMapper userMapper,
|
SystemSettingsAppService systemSettingsAppService) {
|
this.healthCourseAppService = healthCourseAppService;
|
this.healthCourseMapper = healthCourseMapper;
|
this.healthCourseVideoMapper = healthCourseVideoMapper;
|
this.liveCourseCategoryMapper = liveCourseCategoryMapper;
|
this.expertMapper = expertMapper;
|
this.healthCourseDownloadRecordMapper = healthCourseDownloadRecordMapper;
|
this.healthCourseFavoriteRecordMapper = healthCourseFavoriteRecordMapper;
|
this.healthCourseMemberJoinRecordMapper = healthCourseMemberJoinRecordMapper;
|
this.healthCoursePlayRecordMapper = healthCoursePlayRecordMapper;
|
this.appAuthSessionService = appAuthSessionService;
|
this.coursePurchaseOrderMapper = coursePurchaseOrderMapper;
|
this.userMapper = userMapper;
|
this.systemSettingsAppService = systemSettingsAppService;
|
}
|
|
/**
|
* 获取 APP 养生课程列表页数据。
|
*/
|
public List<AppHealthCourseListItemVO> list(Long levelCategoryId, Long difficultyCategoryId) {
|
List<HealthCourseListRow> rows = healthCourseMapper.selectCourseList(
|
null,
|
levelCategoryId,
|
difficultyCategoryId,
|
COURSE_STATUS_ENABLED
|
);
|
if (rows == null || rows.isEmpty()) {
|
return Collections.emptyList();
|
}
|
List<AppHealthCourseListItemVO> result = new ArrayList<AppHealthCourseListItemVO>(rows.size());
|
for (HealthCourseListRow row : rows) {
|
AppHealthCourseListItemVO itemVO = new AppHealthCourseListItemVO();
|
itemVO.setId(row.getId());
|
itemVO.setTitle(trimToEmpty(row.getTitle()));
|
itemVO.setCoverImage(trimToEmpty(row.getCoverImg()));
|
itemVO.setLevelName(trimToEmpty(row.getLevelName()));
|
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;
|
}
|
|
/**
|
* 获取 APP 养生课程详情页数据。
|
*/
|
public AppHealthCourseDetailVO detail(Long courseId) {
|
HealthCourseDetailVO courseDetail = healthCourseAppService.detail(courseId);
|
if (courseDetail.getStatus() == null || courseDetail.getStatus().intValue() != COURSE_STATUS_ENABLED) {
|
throw new IllegalArgumentException("养生课程不存在");
|
}
|
// 进入课程详情即视为一次学习行为,正在学习人数自增;返回值按自增后的口径展示
|
incrementStudyingCount(courseId);
|
|
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.setStudyingCount(Integer.valueOf(defaultNumber(courseDetail.getStudyingCount()) + 1));
|
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()));
|
detailVO.setServicePhone(resolveServicePhone());
|
|
PurchaseStateSnapshot purchaseState = resolvePurchaseState(courseDetail.getId());
|
List<AppHealthCourseVideoVO> videoVOList = toVideoVOList(enabledVideos, purchaseState);
|
detailVO.setVideos(videoVOList);
|
detailVO.setFreeVideoCount(countFreeVideos(videoVOList));
|
detailVO.setPaidVideoCount(countPaidVideos(videoVOList));
|
detailVO.setFavorited(Boolean.valueOf(resolveFavorited(courseDetail.getId())));
|
detailVO.setIsMember(Boolean.valueOf(purchaseState.isMember()));
|
detailVO.setMemberCourseJoined(Boolean.valueOf(purchaseState.isMemberCourseJoined()));
|
detailVO.setCourseOwned(Boolean.valueOf(purchaseState.isCourseOwned()));
|
detailVO.setCoursePurchased(Boolean.valueOf(purchaseState.isCoursePurchased()));
|
return detailVO;
|
}
|
|
/**
|
* 获取当前登录用户的收藏课程列表。
|
*/
|
public List<AppHealthCourseFavoriteItemVO> favorites() {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
List<AppHealthCourseFavoriteRow> rows = healthCourseFavoriteRecordMapper.selectAppFavoriteCourses(userId);
|
if (rows == null || rows.isEmpty()) {
|
return Collections.emptyList();
|
}
|
List<AppHealthCourseFavoriteItemVO> result = new ArrayList<AppHealthCourseFavoriteItemVO>(rows.size());
|
for (AppHealthCourseFavoriteRow row : rows) {
|
AppHealthCourseFavoriteItemVO itemVO = new AppHealthCourseFavoriteItemVO();
|
itemVO.setCourseId(row.getCourseId());
|
itemVO.setTitle(trimToEmpty(row.getTitle()));
|
itemVO.setCoverImage(trimToEmpty(row.getCoverImage()));
|
itemVO.setLevelName(trimToEmpty(row.getLevelName()));
|
itemVO.setDifficultyName(trimToEmpty(row.getDifficultyName()));
|
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);
|
}
|
return result;
|
}
|
|
/**
|
* 获取当前登录用户的已完成下载列表。
|
*/
|
public List<AppHealthCourseDownloadItemVO> downloads() {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
List<AppHealthCourseDownloadRow> rows = healthCourseDownloadRecordMapper.selectAppDownloads(userId);
|
if (rows == null || rows.isEmpty()) {
|
return Collections.emptyList();
|
}
|
List<AppHealthCourseDownloadItemVO> result = new ArrayList<AppHealthCourseDownloadItemVO>(rows.size());
|
for (AppHealthCourseDownloadRow row : rows) {
|
AppHealthCourseDownloadItemVO itemVO = new AppHealthCourseDownloadItemVO();
|
itemVO.setId(row.getId());
|
itemVO.setCourseId(row.getCourseId());
|
itemVO.setCourseVideoId(row.getCourseVideoId());
|
itemVO.setTitle(trimToEmpty(row.getTitle()));
|
itemVO.setSubtitle(trimToEmpty(row.getSubtitle()));
|
itemVO.setCoverImage(trimToEmpty(row.getCoverImage()));
|
itemVO.setDurationSeconds(defaultNumber(row.getDurationSeconds()));
|
itemVO.setDurationText(formatVideoDuration(itemVO.getDurationSeconds()));
|
itemVO.setVideoUrl(trimToEmpty(row.getVideoUrl()));
|
itemVO.setDownloadedAt(row.getDownloadedAt());
|
result.add(itemVO);
|
}
|
return result;
|
}
|
|
/**
|
* 获取当前登录用户的我的课程列表。
|
*/
|
public List<AppMyHealthCourseItemVO> myCourses() {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
List<HealthCourseOwnershipSnapshot> snapshots = listMyCourseSnapshots(userId);
|
if (snapshots.isEmpty()) {
|
return Collections.emptyList();
|
}
|
Map<Long, CourseProgressSnapshot> progressSnapshotMap = buildCourseProgressSnapshotMap(userId, extractCourseIds(snapshots));
|
List<AppMyHealthCourseItemVO> result = new ArrayList<AppMyHealthCourseItemVO>(snapshots.size());
|
for (HealthCourseOwnershipSnapshot snapshot : snapshots) {
|
AppMyHealthCourseItemVO itemVO = new AppMyHealthCourseItemVO();
|
itemVO.setCourseId(snapshot.getCourseId());
|
itemVO.setTitle(trimToEmpty(snapshot.getTitle()));
|
itemVO.setCoverImage(trimToEmpty(snapshot.getCoverImage()));
|
itemVO.setHasHealthTag(Boolean.TRUE);
|
CourseProgressSnapshot progressSnapshot = progressSnapshotMap.get(snapshot.getCourseId());
|
int completedLessons = progressSnapshot == null ? 0 : progressSnapshot.getCompletedLessons();
|
int totalLessons = snapshot.getTotalLessons();
|
itemVO.setCompletedLessons(Integer.valueOf(completedLessons));
|
itemVO.setTotalLessons(Integer.valueOf(totalLessons));
|
itemVO.setProgressPercent(Integer.valueOf(calculateProgressPercent(completedLessons, totalLessons)));
|
itemVO.setSourceType(snapshot.getSourceType());
|
itemVO.setRemovable(Boolean.valueOf(snapshot.isRemovable()));
|
result.add(itemVO);
|
}
|
return result;
|
}
|
|
/**
|
* 删除当前登录用户的会员加课课程。
|
*/
|
@Transactional
|
public void removeMyCourse(Long courseId) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
requireEnabledCourse(courseId);
|
if (findLatestActivePaidCourseOrder(userId, courseId) != null) {
|
throw new IllegalArgumentException("已购买课程不支持移除");
|
}
|
HealthCourseMemberJoinRecord joinRecord = findMemberJoinRecord(courseId, userId);
|
if (joinRecord == null) {
|
throw new IllegalArgumentException("课程不存在或已移除");
|
}
|
healthCourseMemberJoinRecordMapper.update(null, new LambdaUpdateWrapper<HealthCourseMemberJoinRecord>()
|
.eq(HealthCourseMemberJoinRecord::getId, joinRecord.getId())
|
.eq(HealthCourseMemberJoinRecord::getAppUserId, userId)
|
.eq(HealthCourseMemberJoinRecord::getDeleted, 0)
|
.set(HealthCourseMemberJoinRecord::getDeleted, 1));
|
}
|
|
/**
|
* 获取当前登录用户的我的课程详情。
|
*/
|
public AppMyHealthCourseDetailVO myCourseDetail(Long courseId) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
HealthCourseOwnershipSnapshot ownershipSnapshot = requireOwnedCourse(userId, courseId);
|
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(
|
snapshot == null ? 0 : snapshot.getCumulativeWatchSeconds(),
|
itemVO.getDurationSeconds().intValue(),
|
snapshot != null && snapshot.isCompleted()
|
)));
|
itemVO.setLastPlayPositionSeconds(Integer.valueOf(resolveVideoResumePosition(snapshot)));
|
itemVO.setDownloadable(Boolean.valueOf(hasVideoDownloadAccess(userId, courseId, video.getId())));
|
videos.add(itemVO);
|
// 默认播放章节只允许落在当前用户可观看的小节上,未购小节保留展示但不自动选中。
|
if (!locked && snapshot != null && snapshot.getLatestLearnAt() != null
|
&& (latestLearnAt == null || snapshot.getLatestLearnAt().isAfter(latestLearnAt))) {
|
latestLearnAt = snapshot.getLatestLearnAt();
|
currentVideoId = video.getId();
|
}
|
}
|
if (currentVideoId == null) {
|
for (AppMyHealthCourseVideoItemVO itemVO : videos) {
|
if (!Boolean.TRUE.equals(itemVO.getLocked())) {
|
currentVideoId = itemVO.getVideoId();
|
break;
|
}
|
}
|
}
|
|
AppMyHealthCourseDetailVO detailVO = new AppMyHealthCourseDetailVO();
|
detailVO.setCourseId(course.getId());
|
detailVO.setTitle(trimToEmpty(course.getTitle()));
|
detailVO.setCourseTitle(trimToEmpty(course.getTitle()));
|
detailVO.setCoverImage(trimToEmpty(course.getCoverImg()));
|
detailVO.setTotalLessons(Integer.valueOf(enabledVideos.size()));
|
detailVO.setTotalDurationText(formatCourseDuration(resolveTotalDurationSecondsByEntity(course, enabledVideos)));
|
detailVO.setCurrentVideoId(currentVideoId);
|
detailVO.setServicePhone(resolveServicePhone());
|
detailVO.setIntroAvailable(Boolean.TRUE);
|
detailVO.setVideos(videos);
|
return detailVO;
|
}
|
|
/**
|
* 上报当前登录用户的我的课程播放记录。
|
*/
|
@Transactional
|
public AppHealthCoursePlayRecordUploadResultVO uploadMyCoursePlayRecord(Long courseId,
|
Long videoId,
|
AppHealthCoursePlayRecordUploadRequest request) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
requireOwnedCourse(userId, courseId);
|
HealthCourseVideo video = requireEnabledVideo(courseId, videoId);
|
validatePlayRecordRequest(request);
|
|
int videoDurationSeconds = defaultNumber(video.getDurationSeconds());
|
int cumulativeWatchSeconds = resolveExistingCumulativeWatchSeconds(userId, courseId, videoId)
|
+ defaultNumber(request.getDurationSeconds());
|
boolean completed = videoDurationSeconds > 0 && cumulativeWatchSeconds >= videoDurationSeconds;
|
int lastPlayPositionSeconds = completed ? 0 : normalizePlayPosition(request.getLastPlayPositionSeconds(), videoDurationSeconds);
|
|
HealthCoursePlayRecord playRecord = new HealthCoursePlayRecord();
|
playRecord.setCourseId(courseId);
|
playRecord.setVideoId(videoId);
|
playRecord.setAppUserId(userId);
|
playRecord.setPlayStart(request.getPlayStart());
|
playRecord.setPlayEnd(request.getPlayEnd());
|
playRecord.setDurationSeconds(defaultNumber(request.getDurationSeconds()));
|
playRecord.setIsCompleted(completed ? 1 : 0);
|
playRecord.setLastPlayPositionSeconds(lastPlayPositionSeconds);
|
healthCoursePlayRecordMapper.insert(playRecord);
|
|
Map<Long, VideoProgressSnapshot> videoProgressSnapshotMap = buildVideoProgressSnapshotMap(userId, Collections.singletonList(courseId));
|
VideoProgressSnapshot videoProgressSnapshot = videoProgressSnapshotMap.get(videoId);
|
CourseProgressSnapshot courseProgressSnapshot = buildCourseProgressSnapshotMap(userId, Collections.singletonList(courseId)).get(courseId);
|
|
AppHealthCoursePlayRecordUploadResultVO resultVO = new AppHealthCoursePlayRecordUploadResultVO();
|
resultVO.setLastPlayPositionSeconds(Integer.valueOf(resolveVideoResumePosition(videoProgressSnapshot)));
|
resultVO.setCompleted(Boolean.valueOf(videoProgressSnapshot != null && videoProgressSnapshot.isCompleted()));
|
resultVO.setProgressPercent(Integer.valueOf(calculateVideoDisplayProgressPercent(
|
videoProgressSnapshot == null ? 0 : videoProgressSnapshot.getCumulativeWatchSeconds(),
|
videoDurationSeconds,
|
videoProgressSnapshot != null && videoProgressSnapshot.isCompleted()
|
)));
|
int totalLessons = listEnabledVideos(courseId).size();
|
int completedLessons = courseProgressSnapshot == null ? 0 : courseProgressSnapshot.getCompletedLessons();
|
resultVO.setCompletedLessons(Integer.valueOf(completedLessons));
|
resultVO.setTotalLessons(Integer.valueOf(totalLessons));
|
resultVO.setCourseProgressPercent(Integer.valueOf(calculateProgressPercent(completedLessons, totalLessons)));
|
return resultVO;
|
}
|
|
/**
|
* 记录当前登录用户的课程小节下载完成状态。
|
*/
|
@Transactional
|
public void reportDownload(AppHealthCourseDownloadReportRequest request) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
Long courseId = request == null ? null : request.getCourseId();
|
Long courseVideoId = request == null ? null : request.getCourseVideoId();
|
HealthCourse course = requireEnabledCourse(courseId);
|
HealthCourseVideo video = requireEnabledVideo(courseId, courseVideoId);
|
if (trimToEmpty(video.getVideoUrl()).isEmpty()) {
|
throw new IllegalArgumentException("当前课程小节暂未配置视频地址");
|
}
|
if (!hasVideoDownloadAccess(userId, courseId, courseVideoId)) {
|
throw new IllegalArgumentException("当前课程小节未购买,无法下载");
|
}
|
HealthCourseDownloadRecord existingRecord = findActiveDownloadRecord(userId, courseVideoId);
|
if (existingRecord == null) {
|
HealthCourseDownloadRecord record = buildDownloadRecord(userId, course, video);
|
healthCourseDownloadRecordMapper.insert(record);
|
return;
|
}
|
// 重复下载同一小节时只刷新快照与更新时间,避免列表产生重复项。
|
HealthCourseDownloadRecord refreshRecord = buildDownloadRecord(userId, course, video);
|
refreshRecord.setId(existingRecord.getId());
|
healthCourseDownloadRecordMapper.updateById(refreshRecord);
|
}
|
|
/**
|
* 批量删除当前登录用户的下载记录。
|
*/
|
@Transactional
|
public void batchDeleteDownloads(AppHealthCourseDownloadBatchDeleteRequest request) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
Set<Long> recordIds = normalizeDownloadIds(request);
|
if (recordIds.isEmpty()) {
|
throw new IllegalArgumentException("请选择要删除的下载记录");
|
}
|
for (Long recordId : recordIds) {
|
healthCourseDownloadRecordMapper.update(null, new LambdaUpdateWrapper<HealthCourseDownloadRecord>()
|
.eq(HealthCourseDownloadRecord::getId, recordId)
|
.eq(HealthCourseDownloadRecord::getAppUserId, userId)
|
.eq(HealthCourseDownloadRecord::getDeleted, 0)
|
.set(HealthCourseDownloadRecord::getDeleted, 1));
|
}
|
}
|
|
/**
|
* 批量取消当前登录用户的养生课程收藏。
|
*/
|
@Transactional
|
public void batchCancelFavorite(AppHealthCourseFavoriteBatchCancelRequest request) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
Set<Long> courseIds = normalizeCourseIds(request);
|
if (courseIds.isEmpty()) {
|
throw new IllegalArgumentException("请选择要取消收藏的课程");
|
}
|
for (Long courseId : courseIds) {
|
cancelFavoriteIfNeeded(courseId, userId);
|
}
|
}
|
|
/**
|
* 收藏 APP 养生课程。
|
*/
|
@Transactional
|
public AppHealthCourseFavoriteResultVO favorite(Long courseId) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
HealthCourse course = requireEnabledCourse(courseId);
|
HealthCourseFavoriteRecord latestRecord = findLatestFavoriteRecord(courseId, userId);
|
if (isFavorited(latestRecord)) {
|
return buildFavoriteResult(course.getId(), true, course.getFavoriteCount());
|
}
|
insertFavoriteRecord(courseId, userId, ACTION_TYPE_FAVORITE);
|
updateFavoriteCount(courseId, true);
|
return buildFavoriteResult(courseId, true, defaultNumber(course.getFavoriteCount()) + 1);
|
}
|
|
/**
|
* 取消收藏 APP 养生课程。
|
*/
|
@Transactional
|
public AppHealthCourseFavoriteResultVO cancelFavorite(Long courseId) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
HealthCourse course = requireEnabledCourse(courseId);
|
HealthCourseFavoriteRecord latestRecord = findLatestFavoriteRecord(courseId, userId);
|
if (!isFavorited(latestRecord)) {
|
return buildFavoriteResult(course.getId(), false, course.getFavoriteCount());
|
}
|
insertFavoriteRecord(courseId, userId, ACTION_TYPE_CANCEL);
|
updateFavoriteCount(courseId, false);
|
return buildFavoriteResult(courseId, false, Math.max(defaultNumber(course.getFavoriteCount()) - 1, 0));
|
}
|
|
/**
|
* 购买 APP 养生课程整课。
|
*/
|
@Transactional
|
public AppHealthCoursePurchaseResultVO purchaseCourse(Long courseId, AppHealthCoursePurchaseRequest request) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
AppUser user = requireEnabledUser(userId);
|
HealthCourse course = requireEnabledCourse(courseId);
|
String paymentMethod = normalizePaymentMethod(request == null ? null : request.getPaymentMethod());
|
CoursePurchaseOrder existingOrder = findLatestActivePaidCourseOrder(userId, courseId);
|
if (existingOrder != null) {
|
return buildPurchaseResult(existingOrder, true, resolvePurchasedVideoIds(userId, courseId, true));
|
}
|
|
CoursePurchaseOrder order = buildCourseOrder(user, course, paymentMethod, LocalDateTime.now());
|
coursePurchaseOrderMapper.insert(order);
|
return buildPurchaseResult(order, true, resolvePurchasedVideoIds(userId, courseId, true));
|
}
|
|
/**
|
* 购买 APP 养生课程小节。
|
*/
|
@Transactional
|
public AppHealthCoursePurchaseResultVO purchaseVideo(Long courseId, Long videoId, AppHealthCoursePurchaseRequest request) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
AppUser user = requireEnabledUser(userId);
|
HealthCourse course = requireEnabledCourse(courseId);
|
HealthCourseVideo video = requireEnabledVideo(courseId, videoId);
|
String paymentMethod = normalizePaymentMethod(request == null ? null : request.getPaymentMethod());
|
if (isFreeVideo(video.getSalePrice())) {
|
throw new IllegalArgumentException("免费小节无需购买");
|
}
|
|
CoursePurchaseOrder existingCourseOrder = findLatestActivePaidCourseOrder(userId, courseId);
|
if (existingCourseOrder != null) {
|
return buildPurchaseResult(existingCourseOrder, true, resolvePurchasedVideoIds(userId, courseId, true));
|
}
|
|
CoursePurchaseOrder existingVideoOrder = findLatestActivePaidVideoOrder(userId, courseId, videoId);
|
if (existingVideoOrder != null) {
|
return buildPurchaseResult(existingVideoOrder, false, resolvePurchasedVideoIds(userId, courseId, false));
|
}
|
|
CoursePurchaseOrder order = buildVideoOrder(user, course, video, paymentMethod, LocalDateTime.now());
|
coursePurchaseOrderMapper.insert(order);
|
return buildPurchaseResult(order, false, resolvePurchasedVideoIds(userId, courseId, false));
|
}
|
|
/**
|
* 会员将养生课程加入我的课程。
|
*/
|
@Transactional
|
public boolean memberJoinCourse(Long courseId) {
|
Long userId = appAuthSessionService.requireLoginUserId();
|
AppUser user = requireEnabledUser(userId);
|
requireEnabledCourse(courseId);
|
if (!isActiveMember(user)) {
|
throw new IllegalArgumentException("当前用户不是有效会员");
|
}
|
if (findLatestActivePaidCourseOrder(userId, courseId) != null) {
|
return true;
|
}
|
if (findMemberJoinRecord(courseId, userId) != null) {
|
return true;
|
}
|
HealthCourseMemberJoinRecord record = new HealthCourseMemberJoinRecord();
|
record.setCourseId(courseId);
|
record.setAppUserId(userId);
|
record.setJoinedAt(LocalDateTime.now());
|
record.setDeleted(0);
|
healthCourseMemberJoinRecordMapper.insert(record);
|
return true;
|
}
|
|
/**
|
* 获取 APP 养生课程主题筛选项。
|
*/
|
public List<AppHealthCourseCategoryOptionVO> listThemes() {
|
return listCategoriesByType(LiveCourseCategoryAppService.TYPE_LEVEL);
|
}
|
|
/**
|
* 获取 APP 养生课程难度筛选项。
|
*/
|
public List<AppHealthCourseCategoryOptionVO> listDifficulties() {
|
return listCategoriesByType(LiveCourseCategoryAppService.TYPE_DIFFICULTY);
|
}
|
|
/**
|
* 按分类类型读取 APP 养生课程筛选项。
|
*/
|
private List<AppHealthCourseCategoryOptionVO> listCategoriesByType(String categoryType) {
|
List<LiveCourseCategory> categories = liveCourseCategoryMapper.selectList(new LambdaQueryWrapper<LiveCourseCategory>()
|
.eq(LiveCourseCategory::getType, categoryType)
|
.eq(LiveCourseCategory::getDeleted, 0)
|
.orderByAsc(LiveCourseCategory::getSort)
|
.orderByAsc(LiveCourseCategory::getId));
|
if (categories == null || categories.isEmpty()) {
|
return Collections.emptyList();
|
}
|
List<AppHealthCourseCategoryOptionVO> result =
|
new ArrayList<AppHealthCourseCategoryOptionVO>(categories.size());
|
for (LiveCourseCategory category : categories) {
|
AppHealthCourseCategoryOptionVO optionVO = new AppHealthCourseCategoryOptionVO();
|
optionVO.setId(category.getId());
|
optionVO.setName(trimToEmpty(category.getName()));
|
result.add(optionVO);
|
}
|
return result;
|
}
|
|
/**
|
* 汇总我的课程来源,已购课程优先级高于会员加课。
|
*/
|
private List<HealthCourseOwnershipSnapshot> listMyCourseSnapshots(Long userId) {
|
Map<Long, HealthCourseOwnershipSnapshot> snapshotMap = new HashMap<Long, HealthCourseOwnershipSnapshot>();
|
List<CoursePurchaseOrderListRow> paidCourseRows = coursePurchaseOrderMapper.selectPaidCourseSnapshots(userId);
|
if (paidCourseRows != null) {
|
for (CoursePurchaseOrderListRow row : paidCourseRows) {
|
Long courseId = row.getCourseId();
|
if (courseId == null || snapshotMap.containsKey(courseId)) {
|
continue;
|
}
|
HealthCourse course = healthCourseMapper.selectById(courseId);
|
if (!isEnabledCourse(course)) {
|
continue;
|
}
|
snapshotMap.put(courseId, buildOwnershipSnapshot(course, "PURCHASED", false));
|
}
|
}
|
List<Long> memberCourseIds = healthCourseMemberJoinRecordMapper.selectActiveCourseIdsByUserId(userId);
|
if (memberCourseIds != null) {
|
for (Long courseId : memberCourseIds) {
|
if (courseId == null || snapshotMap.containsKey(courseId)) {
|
continue;
|
}
|
HealthCourse course = healthCourseMapper.selectById(courseId);
|
if (!isEnabledCourse(course)) {
|
continue;
|
}
|
snapshotMap.put(courseId, buildOwnershipSnapshot(course, "MEMBER_JOIN", true));
|
}
|
}
|
List<HealthCourseOwnershipSnapshot> snapshots = new ArrayList<HealthCourseOwnershipSnapshot>(snapshotMap.values());
|
Collections.sort(snapshots, Comparator.comparing(HealthCourseOwnershipSnapshot::getCourseId).reversed());
|
return snapshots;
|
}
|
|
/**
|
* 校验课程是否属于当前登录用户的我的课程。
|
*/
|
private HealthCourseOwnershipSnapshot requireOwnedCourse(Long userId, Long courseId) {
|
List<HealthCourseOwnershipSnapshot> snapshots = listMyCourseSnapshots(userId);
|
for (HealthCourseOwnershipSnapshot snapshot : snapshots) {
|
if (snapshot.getCourseId().equals(courseId)) {
|
return snapshot;
|
}
|
}
|
throw new IllegalArgumentException("课程不存在或未加入我的课程");
|
}
|
|
/**
|
* 构建课程归属快照,供我的课程相关接口复用。
|
*/
|
private HealthCourseOwnershipSnapshot buildOwnershipSnapshot(HealthCourse course, String sourceType, boolean removable) {
|
return new HealthCourseOwnershipSnapshot(
|
course.getId(),
|
trimToEmpty(course.getTitle()),
|
trimToEmpty(course.getCoverImg()),
|
countEnabledVideos(course.getId()),
|
sourceType,
|
removable
|
);
|
}
|
|
/**
|
* 批量构建课程整体进度快照。
|
*/
|
private Map<Long, CourseProgressSnapshot> buildCourseProgressSnapshotMap(Long userId, List<Long> courseIds) {
|
if (courseIds == null || courseIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
List<AppMyHealthCourseProgressRow> rows = healthCoursePlayRecordMapper.selectCourseProgressByCourseIds(userId, courseIds);
|
Map<Long, CourseProgressSnapshot> snapshotMap = new HashMap<Long, CourseProgressSnapshot>();
|
if (rows == null) {
|
return snapshotMap;
|
}
|
for (AppMyHealthCourseProgressRow row : rows) {
|
if (row.getCourseId() == null) {
|
continue;
|
}
|
snapshotMap.put(row.getCourseId(), new CourseProgressSnapshot(
|
defaultNumber(row.getCompletedLessons()),
|
row.getLatestLearnAt()
|
));
|
}
|
return snapshotMap;
|
}
|
|
/**
|
* 批量构建章节进度快照,并以章节ID作为索引键。
|
*/
|
private Map<Long, VideoProgressSnapshot> buildVideoProgressSnapshotMap(Long userId, List<Long> courseIds) {
|
if (courseIds == null || courseIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
List<AppMyHealthCourseVideoProgressRow> rows = healthCoursePlayRecordMapper.selectVideoProgressByCourseIds(userId, courseIds);
|
Map<Long, VideoProgressSnapshot> snapshotMap = new HashMap<Long, VideoProgressSnapshot>();
|
if (rows == null) {
|
return snapshotMap;
|
}
|
for (AppMyHealthCourseVideoProgressRow row : rows) {
|
if (row.getVideoId() == null) {
|
continue;
|
}
|
snapshotMap.put(row.getVideoId(), new VideoProgressSnapshot(
|
defaultNumber(row.getCumulativeWatchSeconds()),
|
row.getCompleted() != null && row.getCompleted().intValue() == 1,
|
defaultNumber(row.getLastPlayPositionSeconds()),
|
row.getLatestLearnAt()
|
));
|
}
|
return snapshotMap;
|
}
|
|
/**
|
* 提取课程主键集合,便于批量查询进度。
|
*/
|
private List<Long> extractCourseIds(List<HealthCourseOwnershipSnapshot> snapshots) {
|
List<Long> courseIds = new ArrayList<Long>(snapshots.size());
|
for (HealthCourseOwnershipSnapshot snapshot : snapshots) {
|
courseIds.add(snapshot.getCourseId());
|
}
|
return courseIds;
|
}
|
|
/**
|
* 统计课程启用章节数量。
|
*/
|
private int countEnabledVideos(Long courseId) {
|
return listEnabledVideos(courseId).size();
|
}
|
|
/**
|
* 计算百分比进度。
|
*/
|
private int calculateProgressPercent(int current, int total) {
|
if (total <= 0 || current <= 0) {
|
return 0;
|
}
|
if (current >= total) {
|
return 100;
|
}
|
return Math.max(Math.min((int) Math.round((current * 100.0D) / total), 100), 0);
|
}
|
|
/**
|
* 计算章节播放进度展示值,未完播章节最高仅展示到99%,避免与完播态混淆。
|
*/
|
private int calculateVideoDisplayProgressPercent(int current, int total, boolean completed) {
|
int progressPercent = calculateProgressPercent(current, total);
|
if (!completed && progressPercent >= 100) {
|
return 99;
|
}
|
return progressPercent;
|
}
|
|
/**
|
* 解析章节续播位置,已完播章节再次进入时从头播放。
|
*/
|
private int resolveVideoResumePosition(VideoProgressSnapshot snapshot) {
|
if (snapshot == null || snapshot.isCompleted()) {
|
return 0;
|
}
|
return Math.max(snapshot.getLastPlayPositionSeconds(), 0);
|
}
|
|
/**
|
* 校验播放记录上报请求。
|
*/
|
private void validatePlayRecordRequest(AppHealthCoursePlayRecordUploadRequest request) {
|
if (request == null) {
|
throw new IllegalArgumentException("播放记录不能为空");
|
}
|
if (request.getPlayStart() == null || request.getPlayEnd() == null) {
|
throw new IllegalArgumentException("播放时间不能为空");
|
}
|
if (request.getPlayEnd().isBefore(request.getPlayStart())) {
|
throw new IllegalArgumentException("播放结束时间不能早于开始时间");
|
}
|
}
|
|
/**
|
* 查询当前章节已累计观看时长。
|
*/
|
private int resolveExistingCumulativeWatchSeconds(Long userId, Long courseId, Long videoId) {
|
List<HealthCoursePlayRecord> records = healthCoursePlayRecordMapper.selectList(new LambdaQueryWrapper<HealthCoursePlayRecord>()
|
.eq(HealthCoursePlayRecord::getAppUserId, userId)
|
.eq(HealthCoursePlayRecord::getCourseId, courseId)
|
.eq(HealthCoursePlayRecord::getVideoId, videoId));
|
if (records == null || records.isEmpty()) {
|
return 0;
|
}
|
int sum = 0;
|
for (HealthCoursePlayRecord record : records) {
|
sum += defaultNumber(record.getDurationSeconds());
|
}
|
return sum;
|
}
|
|
/**
|
* 归一化播放位置,避免超出视频时长。
|
*/
|
private int normalizePlayPosition(Integer lastPlayPositionSeconds, int videoDurationSeconds) {
|
int position = defaultNumber(lastPlayPositionSeconds);
|
if (videoDurationSeconds <= 0) {
|
return Math.max(position, 0);
|
}
|
return Math.max(Math.min(position, videoDurationSeconds), 0);
|
}
|
|
/**
|
* 使用实体数据解析课程总时长。
|
*/
|
private Integer resolveTotalDurationSecondsByEntity(HealthCourse course, List<HealthCourseVideo> videos) {
|
Integer totalDurationSeconds = course.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 isEnabledCourse(HealthCourse course) {
|
return course != null
|
&& course.getDeleted() != null
|
&& course.getDeleted().intValue() == 0
|
&& course.getStatus() != null
|
&& course.getStatus().intValue() == COURSE_STATUS_ENABLED;
|
}
|
|
/**
|
* 判断当前请求用户是否已收藏课程;游客统一视为未收藏。
|
*/
|
private boolean resolveFavorited(Long courseId) {
|
if (!appAuthSessionService.isLogin()) {
|
return false;
|
}
|
Long userId = appAuthSessionService.getLoginUserId();
|
if (userId == null) {
|
return false;
|
}
|
return isFavorited(findLatestFavoriteRecord(courseId, userId));
|
}
|
|
/**
|
* 按课程集合规范化批量取消入参。
|
*/
|
private Set<Long> normalizeCourseIds(AppHealthCourseFavoriteBatchCancelRequest request) {
|
if (request == null || request.getCourseIds() == null || request.getCourseIds().isEmpty()) {
|
return Collections.emptySet();
|
}
|
Set<Long> courseIds = new LinkedHashSet<Long>();
|
for (Long courseId : request.getCourseIds()) {
|
if (courseId != null) {
|
courseIds.add(courseId);
|
}
|
}
|
return courseIds;
|
}
|
|
/**
|
* 按下载记录集合规范化批量删除入参。
|
*/
|
private Set<Long> normalizeDownloadIds(AppHealthCourseDownloadBatchDeleteRequest request) {
|
if (request == null || request.getIds() == null || request.getIds().isEmpty()) {
|
return Collections.emptySet();
|
}
|
Set<Long> recordIds = new LinkedHashSet<Long>();
|
for (Long id : request.getIds()) {
|
if (id != null) {
|
recordIds.add(id);
|
}
|
}
|
return recordIds;
|
}
|
|
/**
|
* 在当前课程仍为收藏态时写入取消流水并回收收藏数。
|
*/
|
private void cancelFavoriteIfNeeded(Long courseId, Long userId) {
|
HealthCourse course = healthCourseMapper.selectById(courseId);
|
if (course == null || course.getDeleted() == null || course.getDeleted().intValue() != 0) {
|
return;
|
}
|
HealthCourseFavoriteRecord latestRecord = findLatestFavoriteRecord(courseId, userId);
|
if (!isFavorited(latestRecord)) {
|
return;
|
}
|
insertFavoriteRecord(courseId, userId, ACTION_TYPE_CANCEL);
|
updateFavoriteCount(courseId, false);
|
}
|
|
/**
|
* 查询课程下启用的视频章节。
|
*/
|
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, PurchaseStateSnapshot purchaseState) {
|
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())));
|
boolean purchased = purchaseState.isCourseOwned()
|
|| purchaseState.getPurchasedVideoIds().contains(video.getId());
|
boolean locked = !Boolean.TRUE.equals(videoVO.getFreeAccess()) && !purchased;
|
videoVO.setPurchased(Boolean.valueOf(purchased));
|
videoVO.setLocked(Boolean.valueOf(locked));
|
// 已购买章节需要直接拿到视频地址,前端才能切换为下载入口。
|
videoVO.setVideoUrl(trimToEmpty(video.getVideoUrl()));
|
videoVO.setCreatedAt(video.getCreatedAt());
|
videoVO.setUpdatedAt(video.getUpdatedAt());
|
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 PurchaseStateSnapshot resolvePurchaseState(Long courseId) {
|
if (!appAuthSessionService.isLogin()) {
|
return PurchaseStateSnapshot.empty();
|
}
|
Long userId = appAuthSessionService.getLoginUserId();
|
if (userId == null) {
|
return PurchaseStateSnapshot.empty();
|
}
|
AppUser user = userMapper.selectById(userId);
|
if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
|
return PurchaseStateSnapshot.empty();
|
}
|
CoursePurchaseOrder activeCourseOrder = findLatestActivePaidCourseOrder(userId, courseId);
|
boolean coursePurchased = activeCourseOrder != null;
|
boolean memberCourseJoined = findMemberJoinRecord(courseId, userId) != null;
|
boolean courseOwned = coursePurchased || memberCourseJoined;
|
return new PurchaseStateSnapshot(
|
isActiveMember(user),
|
memberCourseJoined,
|
courseOwned,
|
coursePurchased,
|
resolvePurchasedVideoIds(userId, courseId, courseOwned)
|
);
|
}
|
|
/**
|
* 判断当前用户是否拥有指定小节的下载权限。
|
*/
|
private boolean hasVideoDownloadAccess(Long userId, Long courseId, Long courseVideoId) {
|
PurchaseStateSnapshot purchaseState = resolvePurchaseState(courseId);
|
if (purchaseState.isCourseOwned()) {
|
return true;
|
}
|
return purchaseState.getPurchasedVideoIds().contains(courseVideoId);
|
}
|
|
/**
|
* 读取当前用户对课程仍有效的已购小节集合。
|
*/
|
private List<Long> resolvePurchasedVideoIds(Long userId, Long courseId, boolean courseOwned) {
|
if (courseOwned) {
|
List<HealthCourseVideo> enabledVideos = listEnabledVideos(courseId);
|
List<Long> allVideoIds = new ArrayList<Long>(enabledVideos.size());
|
for (HealthCourseVideo video : enabledVideos) {
|
allVideoIds.add(video.getId());
|
}
|
return allVideoIds;
|
}
|
List<CoursePurchaseOrder> orders = listActivePaidOrders(userId, courseId);
|
Set<Long> purchasedVideoIds = new LinkedHashSet<Long>();
|
for (CoursePurchaseOrder order : orders) {
|
if (PURCHASE_TARGET_CHAPTER.equals(order.getPurchaseTargetType()) && order.getCourseVideoId() != null) {
|
purchasedVideoIds.add(order.getCourseVideoId());
|
}
|
}
|
return new ArrayList<Long>(purchasedVideoIds);
|
}
|
|
/**
|
* 查询用户当前课程下所有有效的已支付订单。
|
*/
|
private List<CoursePurchaseOrder> listActivePaidOrders(Long userId, Long courseId) {
|
List<CoursePurchaseOrder> orders = coursePurchaseOrderMapper.selectList(new LambdaQueryWrapper<CoursePurchaseOrder>()
|
.eq(CoursePurchaseOrder::getAppUserId, userId)
|
.eq(CoursePurchaseOrder::getCourseType, COURSE_TYPE_HEALTH)
|
.eq(CoursePurchaseOrder::getCourseId, courseId)
|
.eq(CoursePurchaseOrder::getStatus, ORDER_STATUS_PAID)
|
.eq(CoursePurchaseOrder::getDeleted, 0)
|
.orderByDesc(CoursePurchaseOrder::getPaidAt)
|
.orderByDesc(CoursePurchaseOrder::getId));
|
return orders == null ? Collections.<CoursePurchaseOrder>emptyList() : orders;
|
}
|
|
/**
|
* 查询用户当前课程最近一条有效整课订单。
|
*/
|
private CoursePurchaseOrder findLatestActivePaidCourseOrder(Long userId, Long courseId) {
|
return coursePurchaseOrderMapper.selectOne(new LambdaQueryWrapper<CoursePurchaseOrder>()
|
.eq(CoursePurchaseOrder::getAppUserId, userId)
|
.eq(CoursePurchaseOrder::getCourseType, COURSE_TYPE_HEALTH)
|
.eq(CoursePurchaseOrder::getPurchaseTargetType, PURCHASE_TARGET_COURSE)
|
.eq(CoursePurchaseOrder::getCourseId, courseId)
|
.eq(CoursePurchaseOrder::getStatus, ORDER_STATUS_PAID)
|
.eq(CoursePurchaseOrder::getDeleted, 0)
|
.orderByDesc(CoursePurchaseOrder::getPaidAt)
|
.orderByDesc(CoursePurchaseOrder::getId)
|
.last("LIMIT 1"));
|
}
|
|
/**
|
* 查询用户当前课程最近一条有效小节订单。
|
*/
|
private CoursePurchaseOrder findLatestActivePaidVideoOrder(Long userId, Long courseId, Long videoId) {
|
return coursePurchaseOrderMapper.selectOne(new LambdaQueryWrapper<CoursePurchaseOrder>()
|
.eq(CoursePurchaseOrder::getAppUserId, userId)
|
.eq(CoursePurchaseOrder::getCourseType, COURSE_TYPE_HEALTH)
|
.eq(CoursePurchaseOrder::getPurchaseTargetType, PURCHASE_TARGET_CHAPTER)
|
.eq(CoursePurchaseOrder::getCourseId, courseId)
|
.eq(CoursePurchaseOrder::getCourseVideoId, videoId)
|
.eq(CoursePurchaseOrder::getStatus, ORDER_STATUS_PAID)
|
.eq(CoursePurchaseOrder::getDeleted, 0)
|
.orderByDesc(CoursePurchaseOrder::getPaidAt)
|
.orderByDesc(CoursePurchaseOrder::getId)
|
.last("LIMIT 1"));
|
}
|
|
/**
|
* 查询当前用户该小节最近一条未删除下载记录。
|
*/
|
private HealthCourseDownloadRecord findActiveDownloadRecord(Long userId, Long courseVideoId) {
|
return healthCourseDownloadRecordMapper.selectOne(new LambdaQueryWrapper<HealthCourseDownloadRecord>()
|
.eq(HealthCourseDownloadRecord::getAppUserId, userId)
|
.eq(HealthCourseDownloadRecord::getCourseVideoId, courseVideoId)
|
.eq(HealthCourseDownloadRecord::getDeleted, 0)
|
.orderByDesc(HealthCourseDownloadRecord::getUpdatedAt)
|
.orderByDesc(HealthCourseDownloadRecord::getId)
|
.last("LIMIT 1"));
|
}
|
|
/**
|
* 组装课程小节下载记录快照。
|
*/
|
private HealthCourseDownloadRecord buildDownloadRecord(Long userId, HealthCourse course, HealthCourseVideo video) {
|
HealthCourseDownloadRecord record = new HealthCourseDownloadRecord();
|
record.setAppUserId(userId);
|
record.setCourseId(course.getId());
|
record.setCourseVideoId(video.getId());
|
record.setCourseTitleSnapshot(trimToEmpty(course.getTitle()));
|
record.setChapterNameSnapshot(trimToEmpty(video.getChapterName()));
|
record.setCoverImgSnapshot(trimToEmpty(course.getCoverImg()));
|
record.setVideoUrlSnapshot(trimToEmpty(video.getVideoUrl()));
|
record.setDurationSecondsSnapshot(defaultNumber(video.getDurationSeconds()));
|
record.setDeleted(0);
|
return record;
|
}
|
|
/**
|
* 查询用户当前课程有效的会员加课记录。
|
*/
|
private HealthCourseMemberJoinRecord findMemberJoinRecord(Long courseId, Long userId) {
|
return healthCourseMemberJoinRecordMapper.selectOne(new LambdaQueryWrapper<HealthCourseMemberJoinRecord>()
|
.eq(HealthCourseMemberJoinRecord::getCourseId, courseId)
|
.eq(HealthCourseMemberJoinRecord::getAppUserId, userId)
|
.eq(HealthCourseMemberJoinRecord::getDeleted, 0)
|
.orderByDesc(HealthCourseMemberJoinRecord::getJoinedAt)
|
.orderByDesc(HealthCourseMemberJoinRecord::getId)
|
.last("LIMIT 1"));
|
}
|
|
/**
|
* 组装整课订单数据,支付方案落地前直接记为已支付。
|
*/
|
private CoursePurchaseOrder buildCourseOrder(AppUser user, HealthCourse course, String paymentMethod, LocalDateTime now) {
|
CoursePurchaseOrder order = new CoursePurchaseOrder();
|
order.setOrderNo(buildOrderNo(user.getId(), now));
|
order.setAppUserId(user.getId());
|
order.setUserNickname(resolveUserNickname(user));
|
order.setUserPhone(trimToEmpty(user.getPhone()));
|
order.setCourseType(COURSE_TYPE_HEALTH);
|
order.setPurchaseTargetType(PURCHASE_TARGET_COURSE);
|
order.setCourseId(course.getId());
|
order.setCourseVideoId(null);
|
order.setProductNameSnapshot(trimToEmpty(course.getTitle()));
|
order.setCourseTitleSnapshot(trimToEmpty(course.getTitle()));
|
order.setChapterNameSnapshot(null);
|
order.setCoverImgSnapshot(trimToEmpty(course.getCoverImg()));
|
order.setOrderAmount(defaultPrice(course.getSalePrice()));
|
order.setPaymentMethod(paymentMethod);
|
order.setRefundAmount(null);
|
order.setRefundRemark(null);
|
order.setStatus(ORDER_STATUS_PAID);
|
order.setPaidAt(now);
|
order.setRefundedAt(null);
|
order.setCancelledAt(null);
|
order.setDeleted(0);
|
return order;
|
}
|
|
/**
|
* 组装课程小节订单数据,支付方案落地前直接记为已支付。
|
*/
|
private CoursePurchaseOrder buildVideoOrder(AppUser user,
|
HealthCourse course,
|
HealthCourseVideo video,
|
String paymentMethod,
|
LocalDateTime now) {
|
CoursePurchaseOrder order = new CoursePurchaseOrder();
|
order.setOrderNo(buildOrderNo(user.getId(), now));
|
order.setAppUserId(user.getId());
|
order.setUserNickname(resolveUserNickname(user));
|
order.setUserPhone(trimToEmpty(user.getPhone()));
|
order.setCourseType(COURSE_TYPE_HEALTH);
|
order.setPurchaseTargetType(PURCHASE_TARGET_CHAPTER);
|
order.setCourseId(course.getId());
|
order.setCourseVideoId(video.getId());
|
order.setProductNameSnapshot(trimToEmpty(course.getTitle()) + "-" + trimToEmpty(video.getChapterName()));
|
order.setCourseTitleSnapshot(trimToEmpty(course.getTitle()));
|
order.setChapterNameSnapshot(trimToEmpty(video.getChapterName()));
|
order.setCoverImgSnapshot(trimToEmpty(course.getCoverImg()));
|
order.setOrderAmount(defaultPrice(video.getSalePrice()));
|
order.setPaymentMethod(paymentMethod);
|
order.setRefundAmount(null);
|
order.setRefundRemark(null);
|
order.setStatus(ORDER_STATUS_PAID);
|
order.setPaidAt(now);
|
order.setRefundedAt(null);
|
order.setCancelledAt(null);
|
order.setDeleted(0);
|
return order;
|
}
|
|
/**
|
* 组装购买结果响应,供详情页直接回写购买态。
|
*/
|
private AppHealthCoursePurchaseResultVO buildPurchaseResult(CoursePurchaseOrder order,
|
boolean coursePurchased,
|
List<Long> purchasedVideoIds) {
|
AppHealthCoursePurchaseResultVO resultVO = new AppHealthCoursePurchaseResultVO();
|
resultVO.setOrderId(order == null ? null : order.getId());
|
resultVO.setOrderNo(order == null ? null : order.getOrderNo());
|
resultVO.setPaymentMethod(order == null ? null : order.getPaymentMethod());
|
resultVO.setStatus(order == null ? ORDER_STATUS_PAID : order.getStatus());
|
resultVO.setPaidAt(order == null ? null : order.getPaidAt());
|
resultVO.setCoursePurchased(Boolean.valueOf(coursePurchased));
|
resultVO.setPurchasedVideoIds(purchasedVideoIds == null ? Collections.<Long>emptyList() : purchasedVideoIds);
|
return resultVO;
|
}
|
|
/**
|
* 校验当前登录用户存在且可用。
|
*/
|
private AppUser requireEnabledUser(Long userId) {
|
AppUser user = userMapper.selectById(userId);
|
if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
|
throw new IllegalArgumentException("用户不存在");
|
}
|
if (user.getStatus() == null || user.getStatus().intValue() != 1) {
|
throw new IllegalArgumentException("用户已禁用");
|
}
|
return user;
|
}
|
|
/**
|
* 校验课程小节存在、归属正确且已启用。
|
*/
|
private HealthCourseVideo requireEnabledVideo(Long courseId, Long videoId) {
|
HealthCourseVideo video = healthCourseVideoMapper.selectById(videoId);
|
if (video == null || video.getDeleted() == null || video.getDeleted().intValue() != 0
|
|| video.getStatus() == null || video.getStatus().intValue() != VIDEO_STATUS_ENABLED) {
|
throw new IllegalArgumentException("课程小节不存在");
|
}
|
if (!courseId.equals(video.getCourseId())) {
|
throw new IllegalArgumentException("课程小节不存在");
|
}
|
return video;
|
}
|
|
/**
|
* 规范化支付方式入参,仅允许微信或支付宝。
|
*/
|
private String normalizePaymentMethod(String paymentMethod) {
|
String normalized = trimToEmpty(paymentMethod).toUpperCase();
|
if (PAYMENT_METHOD_WECHAT.equals(normalized) || PAYMENT_METHOD_ALIPAY.equals(normalized)) {
|
return normalized;
|
}
|
throw new IllegalArgumentException("支付方式不合法");
|
}
|
|
/**
|
* 生成课程订单号。
|
*/
|
private String buildOrderNo(Long userId, LocalDateTime now) {
|
String seed = String.valueOf(Math.abs((userId == null ? 0L : userId.longValue()) % 10000L));
|
int nanoTail = Math.abs(now.getNano() / 1000) % 1000000;
|
return "CPO" + ORDER_NO_TIME_FORMATTER.format(now)
|
+ String.format("%06d", nanoTail)
|
+ String.format("%04d", Integer.parseInt(seed));
|
}
|
|
/**
|
* 优先使用昵称快照,没有昵称时降级到微信昵称或用户编号。
|
*/
|
private String resolveUserNickname(AppUser user) {
|
String nickname = trimToEmpty(user.getNickname());
|
if (!nickname.isEmpty()) {
|
return nickname;
|
}
|
String wechatNickname = trimToEmpty(user.getWechatNickname());
|
if (!wechatNickname.isEmpty()) {
|
return wechatNickname;
|
}
|
return "用户" + user.getId();
|
}
|
|
/**
|
* 判断用户当前是否仍属于有效会员。
|
*/
|
private boolean isActiveMember(AppUser user) {
|
return user != null && user.getMemberExpireAt() != null && user.getMemberExpireAt().isAfter(LocalDateTime.now());
|
}
|
|
/**
|
* 读取用户对课程的最新收藏操作记录。
|
*/
|
private HealthCourseFavoriteRecord findLatestFavoriteRecord(Long courseId, Long userId) {
|
return healthCourseFavoriteRecordMapper.selectOne(new LambdaQueryWrapper<HealthCourseFavoriteRecord>()
|
.eq(HealthCourseFavoriteRecord::getCourseId, courseId)
|
.eq(HealthCourseFavoriteRecord::getAppUserId, userId)
|
.orderByDesc(HealthCourseFavoriteRecord::getActionTime)
|
.orderByDesc(HealthCourseFavoriteRecord::getId)
|
.last("LIMIT 1"));
|
}
|
|
/**
|
* 判断最新操作是否表示当前仍处于已收藏状态。
|
*/
|
private boolean isFavorited(HealthCourseFavoriteRecord latestRecord) {
|
return latestRecord != null && ACTION_TYPE_FAVORITE.equalsIgnoreCase(trimToEmpty(latestRecord.getActionType()));
|
}
|
|
/**
|
* 写入一条收藏流水记录。
|
*/
|
private void insertFavoriteRecord(Long courseId, Long userId, String actionType) {
|
HealthCourseFavoriteRecord record = new HealthCourseFavoriteRecord();
|
record.setCourseId(courseId);
|
record.setAppUserId(userId);
|
record.setActionType(actionType);
|
record.setActionTime(LocalDateTime.now());
|
healthCourseFavoriteRecordMapper.insert(record);
|
}
|
|
/**
|
* 更新课程收藏计数,仅在状态发生切换时调用。
|
*/
|
private void updateFavoriteCount(Long courseId, boolean increase) {
|
LambdaUpdateWrapper<HealthCourse> updateWrapper = new LambdaUpdateWrapper<HealthCourse>()
|
.eq(HealthCourse::getId, courseId);
|
if (increase) {
|
updateWrapper.setSql("favorite_count = COALESCE(favorite_count, 0) + 1");
|
} else {
|
updateWrapper.setSql("favorite_count = GREATEST(COALESCE(favorite_count, 0) - 1, 0)");
|
}
|
healthCourseMapper.update(null, updateWrapper);
|
}
|
|
/**
|
* 正在学习人数自增 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) {
|
AppHealthCourseFavoriteResultVO resultVO = new AppHealthCourseFavoriteResultVO();
|
resultVO.setCourseId(courseId);
|
resultVO.setFavorited(Boolean.valueOf(favorited));
|
resultVO.setFavoriteCount(Integer.valueOf(Math.max(defaultNumber(favoriteCount), 0)));
|
return resultVO;
|
}
|
|
/**
|
* 校验课程存在且为启用状态。
|
*/
|
private HealthCourse requireEnabledCourse(Long courseId) {
|
HealthCourse course = healthCourseMapper.selectById(courseId);
|
if (course == null || course.getDeleted() == null || course.getDeleted().intValue() != 0
|
|| course.getStatus() == null || course.getStatus().intValue() != COURSE_STATUS_ENABLED) {
|
throw new IllegalArgumentException("养生课程不存在");
|
}
|
return course;
|
}
|
|
/**
|
* 兜底空价格字段,避免前端价格区展示异常。
|
*/
|
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;
|
}
|
|
/**
|
* 读取当前系统配置的客服电话,未配置时返回空字符串。
|
*/
|
private String resolveServicePhone() {
|
SystemSettingsVO systemSettings = systemSettingsAppService.current();
|
if (systemSettings == null) {
|
return "";
|
}
|
return trimToEmpty(systemSettings.getServicePhone());
|
}
|
|
/**
|
* APP 课程购买态快照。
|
*/
|
private static final class PurchaseStateSnapshot {
|
|
private final boolean member;
|
private final boolean memberCourseJoined;
|
private final boolean courseOwned;
|
private final boolean coursePurchased;
|
private final List<Long> purchasedVideoIds;
|
|
private PurchaseStateSnapshot(boolean member,
|
boolean memberCourseJoined,
|
boolean courseOwned,
|
boolean coursePurchased,
|
List<Long> purchasedVideoIds) {
|
this.member = member;
|
this.memberCourseJoined = memberCourseJoined;
|
this.courseOwned = courseOwned;
|
this.coursePurchased = coursePurchased;
|
this.purchasedVideoIds = purchasedVideoIds == null ? Collections.<Long>emptyList() : purchasedVideoIds;
|
}
|
|
private static PurchaseStateSnapshot empty() {
|
return new PurchaseStateSnapshot(false, false, false, false, Collections.<Long>emptyList());
|
}
|
|
private boolean isMember() {
|
return member;
|
}
|
|
private boolean isMemberCourseJoined() {
|
return memberCourseJoined;
|
}
|
|
private boolean isCourseOwned() {
|
return courseOwned;
|
}
|
|
private boolean isCoursePurchased() {
|
return coursePurchased;
|
}
|
|
private List<Long> getPurchasedVideoIds() {
|
return purchasedVideoIds;
|
}
|
}
|
|
/**
|
* 我的课程来源快照。
|
*/
|
private static final class HealthCourseOwnershipSnapshot {
|
|
private final Long courseId;
|
private final String title;
|
private final String coverImage;
|
private final int totalLessons;
|
private final String sourceType;
|
private final boolean removable;
|
|
private HealthCourseOwnershipSnapshot(Long courseId,
|
String title,
|
String coverImage,
|
int totalLessons,
|
String sourceType,
|
boolean removable) {
|
this.courseId = courseId;
|
this.title = title;
|
this.coverImage = coverImage;
|
this.totalLessons = totalLessons;
|
this.sourceType = sourceType;
|
this.removable = removable;
|
}
|
|
private Long getCourseId() {
|
return courseId;
|
}
|
|
private String getTitle() {
|
return title;
|
}
|
|
private String getCoverImage() {
|
return coverImage;
|
}
|
|
private int getTotalLessons() {
|
return totalLessons;
|
}
|
|
private String getSourceType() {
|
return sourceType;
|
}
|
|
private boolean isRemovable() {
|
return removable;
|
}
|
}
|
|
/**
|
* 我的课程整体进度快照。
|
*/
|
private static final class CourseProgressSnapshot {
|
|
private final int completedLessons;
|
private final LocalDateTime latestLearnAt;
|
|
private CourseProgressSnapshot(int completedLessons, LocalDateTime latestLearnAt) {
|
this.completedLessons = completedLessons;
|
this.latestLearnAt = latestLearnAt;
|
}
|
|
private int getCompletedLessons() {
|
return completedLessons;
|
}
|
|
private LocalDateTime getLatestLearnAt() {
|
return latestLearnAt;
|
}
|
}
|
|
/**
|
* 我的课程章节进度快照。
|
*/
|
private static final class VideoProgressSnapshot {
|
|
private final int cumulativeWatchSeconds;
|
private final boolean completed;
|
private final int lastPlayPositionSeconds;
|
private final LocalDateTime latestLearnAt;
|
|
private VideoProgressSnapshot(int cumulativeWatchSeconds,
|
boolean completed,
|
int lastPlayPositionSeconds,
|
LocalDateTime latestLearnAt) {
|
this.cumulativeWatchSeconds = cumulativeWatchSeconds;
|
this.completed = completed;
|
this.lastPlayPositionSeconds = lastPlayPositionSeconds;
|
this.latestLearnAt = latestLearnAt;
|
}
|
|
private int getCumulativeWatchSeconds() {
|
return cumulativeWatchSeconds;
|
}
|
|
private boolean isCompleted() {
|
return completed;
|
}
|
|
private int getLastPlayPositionSeconds() {
|
return lastPlayPositionSeconds;
|
}
|
|
private LocalDateTime getLatestLearnAt() {
|
return latestLearnAt;
|
}
|
}
|
}
|