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.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.AppHealthCourseExpertVO; 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.AppHealthCoursePurchaseResultVO; import com.tcm.training.admin.app.course.vo.AppHealthCourseVideoVO; 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.HealthCourseFavoriteRecord; import com.tcm.training.admin.live.entity.HealthCourseMemberJoinRecord; 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.HealthCourseFavoriteRecordMapper; import com.tcm.training.admin.live.mapper.HealthCourseMapper; import com.tcm.training.admin.live.mapper.HealthCourseMemberJoinRecordMapper; import com.tcm.training.admin.live.mapper.HealthCourseVideoMapper; import com.tcm.training.admin.live.mapper.LiveCourseCategoryMapper; 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.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.LinkedHashSet; import java.util.List; 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 HealthCourseFavoriteRecordMapper healthCourseFavoriteRecordMapper; private final HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper; 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, HealthCourseFavoriteRecordMapper healthCourseFavoriteRecordMapper, HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper, AppAuthSessionService appAuthSessionService, CoursePurchaseOrderMapper coursePurchaseOrderMapper, UserMapper userMapper, SystemSettingsAppService systemSettingsAppService) { this.healthCourseAppService = healthCourseAppService; this.healthCourseMapper = healthCourseMapper; this.healthCourseVideoMapper = healthCourseVideoMapper; this.liveCourseCategoryMapper = liveCourseCategoryMapper; this.expertMapper = expertMapper; this.healthCourseFavoriteRecordMapper = healthCourseFavoriteRecordMapper; this.healthCourseMemberJoinRecordMapper = healthCourseMemberJoinRecordMapper; this.appAuthSessionService = appAuthSessionService; this.coursePurchaseOrderMapper = coursePurchaseOrderMapper; this.userMapper = userMapper; this.systemSettingsAppService = systemSettingsAppService; } /** * 获取 APP 养生课程列表页数据。 */ public List list(Long levelCategoryId, Long difficultyCategoryId) { List rows = healthCourseMapper.selectCourseList( null, levelCategoryId, difficultyCategoryId, COURSE_STATUS_ENABLED ); if (rows == null || rows.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList(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; } /** * 获取 APP 养生课程详情页数据。 */ public AppHealthCourseDetailVO detail(Long courseId) { HealthCourseDetailVO courseDetail = healthCourseAppService.detail(courseId); if (courseDetail.getStatus() == null || courseDetail.getStatus().intValue() != COURSE_STATUS_ENABLED) { throw new IllegalArgumentException("养生课程不存在"); } List enabledVideos = listEnabledVideos(courseId); AppHealthCourseDetailVO detailVO = new AppHealthCourseDetailVO(); detailVO.setId(courseDetail.getId()); detailVO.setTitle(trimToEmpty(courseDetail.getTitle())); detailVO.setBannerImages(resolveBannerImages(courseDetail)); detailVO.setLevelName(trimToEmpty(courseDetail.getLevelName())); detailVO.setDifficultyName(trimToEmpty(courseDetail.getDifficultyName())); detailVO.setSalePrice(defaultPrice(courseDetail.getSalePrice())); detailVO.setLearnUserCount(defaultNumber(courseDetail.getLearnUserCount())); detailVO.setSummary(trimToEmpty(courseDetail.getSummary())); detailVO.setTargetUsers(trimToEmpty(courseDetail.getTargetUsers())); detailVO.setContent(trimToEmpty(courseDetail.getContent())); detailVO.setVideoCount(Integer.valueOf(enabledVideos.size())); detailVO.setTotalDurationSeconds(resolveTotalDurationSeconds(courseDetail, enabledVideos)); detailVO.setTotalDurationText(formatCourseDuration(detailVO.getTotalDurationSeconds())); detailVO.setExpert(resolveExpert(courseDetail.getExpert() == null ? null : courseDetail.getExpert().getId())); detailVO.setServicePhone(resolveServicePhone()); PurchaseStateSnapshot purchaseState = resolvePurchaseState(courseDetail.getId()); List 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; } /** * 收藏 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()); if (course.getSalePrice() == null || course.getSalePrice().compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("当前课程无需购买"); } CoursePurchaseOrder existingOrder = findLatestActivePaidCourseOrder(userId, courseId); if (existingOrder != null) { return buildPurchaseResult(existingOrder, true, resolvePurchasedVideoIds(userId, courseId, true)); } 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 listThemes() { return listCategoriesByType(LiveCourseCategoryAppService.TYPE_LEVEL); } /** * 获取 APP 养生课程难度筛选项。 */ public List listDifficulties() { return listCategoriesByType(LiveCourseCategoryAppService.TYPE_DIFFICULTY); } /** * 按分类类型读取 APP 养生课程筛选项。 */ private List listCategoriesByType(String categoryType) { List categories = liveCourseCategoryMapper.selectList(new LambdaQueryWrapper() .eq(LiveCourseCategory::getType, categoryType) .eq(LiveCourseCategory::getDeleted, 0) .orderByAsc(LiveCourseCategory::getSort) .orderByAsc(LiveCourseCategory::getId)); if (categories == null || categories.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList(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 boolean resolveFavorited(Long courseId) { if (!appAuthSessionService.isLogin()) { return false; } Long userId = appAuthSessionService.getLoginUserId(); if (userId == null) { return false; } return isFavorited(findLatestFavoriteRecord(courseId, userId)); } /** * 查询课程下启用的视频章节。 */ private List listEnabledVideos(Long courseId) { List records = healthCourseVideoMapper.selectList(new LambdaQueryWrapper() .eq(HealthCourseVideo::getCourseId, courseId) .eq(HealthCourseVideo::getDeleted, 0) .eq(HealthCourseVideo::getStatus, VIDEO_STATUS_ENABLED) .orderByAsc(HealthCourseVideo::getSort) .orderByAsc(HealthCourseVideo::getId)); return records == null ? Collections.emptyList() : records; } /** * 组装课程详情页需要展示的轮播图列表。 */ private List resolveBannerImages(HealthCourseDetailVO courseDetail) { List bannerImages = new ArrayList(); if (courseDetail.getBannerImgs() != null) { for (String bannerImage : courseDetail.getBannerImgs()) { String normalizedImage = trimToEmpty(bannerImage); if (!normalizedImage.isEmpty()) { bannerImages.add(normalizedImage); } } } // 课程未配置轮播图时,回退到封面图,避免前端轮播组件出现空白页。 if (bannerImages.isEmpty()) { String coverImage = trimToEmpty(courseDetail.getCoverImg()); if (!coverImage.isEmpty()) { bannerImages.add(coverImage); } } return bannerImages; } /** * 组装课程详情页专家卡片信息。 */ private AppHealthCourseExpertVO resolveExpert(Long expertId) { if (expertId == null) { return null; } Expert expert = expertMapper.selectOne(new LambdaQueryWrapper() .eq(Expert::getId, expertId) .eq(Expert::getDeleted, 0) .eq(Expert::getStatus, COURSE_STATUS_ENABLED) .last("LIMIT 1")); if (expert == null) { return null; } AppHealthCourseExpertVO expertVO = new AppHealthCourseExpertVO(); expertVO.setId(expert.getId()); expertVO.setName(trimToEmpty(expert.getName())); expertVO.setAvatar(trimToEmpty(expert.getAvatar())); expertVO.setSpecialty(trimToEmpty(expert.getSpecialty())); return expertVO; } /** * 将启用视频列表转换为 APP 详情页可直接渲染的数据结构。 */ private List toVideoVOList(List videos, PurchaseStateSnapshot purchaseState) { if (videos == null || videos.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList(videos.size()); for (HealthCourseVideo video : videos) { AppHealthCourseVideoVO videoVO = new AppHealthCourseVideoVO(); videoVO.setId(video.getId()); videoVO.setChapterName(trimToEmpty(video.getChapterName())); videoVO.setDurationSeconds(defaultNumber(video.getDurationSeconds())); videoVO.setDurationText(formatVideoDuration(videoVO.getDurationSeconds())); videoVO.setSalePrice(defaultPrice(video.getSalePrice())); // 前端课程列表直接按该字段切分免费/付费列表,避免再次复制业务判断。 videoVO.setFreeAccess(Boolean.valueOf(isFreeVideo(videoVO.getSalePrice()))); 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())); result.add(videoVO); } return result; } /** * 统计免费章节数量。 */ private Integer countFreeVideos(List videos) { int count = 0; for (AppHealthCourseVideoVO video : videos) { if (Boolean.TRUE.equals(video.getFreeAccess())) { count++; } } return Integer.valueOf(count); } /** * 统计付费章节数量。 */ private Integer countPaidVideos(List videos) { int count = 0; for (AppHealthCourseVideoVO video : videos) { if (!Boolean.TRUE.equals(video.getFreeAccess())) { count++; } } return Integer.valueOf(count); } /** * 解析课程总时长,优先复用课程表已汇总的时长字段。 */ private Integer resolveTotalDurationSeconds(HealthCourseDetailVO courseDetail, List videos) { Integer totalDurationSeconds = courseDetail.getTotalDurationSeconds(); if (totalDurationSeconds != null && totalDurationSeconds.intValue() > 0) { return totalDurationSeconds; } int sum = 0; for (HealthCourseVideo video : videos) { sum += defaultNumber(video.getDurationSeconds()); } return Integer.valueOf(sum); } /** * 判断视频是否属于免费章节。 */ private boolean isFreeVideo(BigDecimal salePrice) { return salePrice == null || salePrice.compareTo(BigDecimal.ZERO) <= 0; } /** * 汇总当前登录用户的课程购买状态。 */ private 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 List resolvePurchasedVideoIds(Long userId, Long courseId, boolean courseOwned) { if (courseOwned) { List enabledVideos = listEnabledVideos(courseId); List allVideoIds = new ArrayList(enabledVideos.size()); for (HealthCourseVideo video : enabledVideos) { allVideoIds.add(video.getId()); } return allVideoIds; } List orders = listActivePaidOrders(userId, courseId); Set purchasedVideoIds = new LinkedHashSet(); for (CoursePurchaseOrder order : orders) { if (PURCHASE_TARGET_CHAPTER.equals(order.getPurchaseTargetType()) && order.getCourseVideoId() != null) { purchasedVideoIds.add(order.getCourseVideoId()); } } return new ArrayList(purchasedVideoIds); } /** * 查询用户当前课程下所有有效的已支付订单。 */ private List listActivePaidOrders(Long userId, Long courseId) { List orders = coursePurchaseOrderMapper.selectList(new LambdaQueryWrapper() .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.emptyList() : orders; } /** * 查询用户当前课程最近一条有效整课订单。 */ private CoursePurchaseOrder findLatestActivePaidCourseOrder(Long userId, Long courseId) { return coursePurchaseOrderMapper.selectOne(new LambdaQueryWrapper() .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() .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 HealthCourseMemberJoinRecord findMemberJoinRecord(Long courseId, Long userId) { return healthCourseMemberJoinRecordMapper.selectOne(new LambdaQueryWrapper() .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 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.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() .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 updateWrapper = new LambdaUpdateWrapper() .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; } /** * 兜底空价格字段,避免前端价格区展示异常。 */ 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 purchasedVideoIds; private PurchaseStateSnapshot(boolean member, boolean memberCourseJoined, boolean courseOwned, boolean coursePurchased, List purchasedVideoIds) { this.member = member; this.memberCourseJoined = memberCourseJoined; this.courseOwned = courseOwned; this.coursePurchased = coursePurchased; this.purchasedVideoIds = purchasedVideoIds == null ? Collections.emptyList() : purchasedVideoIds; } private static PurchaseStateSnapshot empty() { return new PurchaseStateSnapshot(false, false, false, false, Collections.emptyList()); } private boolean isMember() { return member; } private boolean isMemberCourseJoined() { return memberCourseJoined; } private boolean isCourseOwned() { return courseOwned; } private boolean isCoursePurchased() { return coursePurchased; } private List getPurchasedVideoIds() { return purchasedVideoIds; } } }