| | |
| | | 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 养生课程详情聚合服务。 |
| | |
| | | |
| | | 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, |
| | | ExpertMapper expertMapper) { |
| | | 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())); |
| | | result.add(itemVO); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | |
| | | detailVO.setTotalDurationSeconds(resolveTotalDurationSeconds(courseDetail, enabledVideos)); |
| | | detailVO.setTotalDurationText(formatCourseDuration(detailVO.getTotalDurationSeconds())); |
| | | detailVO.setExpert(resolveExpert(courseDetail.getExpert() == null ? null : courseDetail.getExpert().getId())); |
| | | detailVO.setServicePhone(resolveServicePhone()); |
| | | |
| | | List<AppHealthCourseVideoVO> videoVOList = toVideoVOList(enabledVideos); |
| | | 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.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); |
| | | } |
| | | |
| | | /** |
| | |
| | | /** |
| | | * 将启用视频列表转换为 APP 详情页可直接渲染的数据结构。 |
| | | */ |
| | | private List<AppHealthCourseVideoVO> toVideoVOList(List<HealthCourseVideo> videos) { |
| | | private List<AppHealthCourseVideoVO> toVideoVOList(List<HealthCourseVideo> videos, PurchaseStateSnapshot purchaseState) { |
| | | if (videos == null || videos.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | |
| | | 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 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); |
| | | } |
| | | |
| | | /** |
| | | * 构造收藏操作返回结果,避免控制器层重复拼装。 |
| | | */ |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |