From 0bc54f2346e919644f7b14c1e91dbb789f59b063 Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期二, 14 四月 2026 22:21:11 +0800
Subject: [PATCH] feat:APP课程模块、健康科普

---
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java |  581 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 578 insertions(+), 3 deletions(-)

diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java
index d5fcb46..4d4f298 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java
@@ -1,28 +1,49 @@
 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 养生课程详情聚合服务。
@@ -32,12 +53,27 @@
 
     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 养生课程详情所需依赖。
@@ -46,12 +82,24 @@
                                   HealthCourseMapper healthCourseMapper,
                                   HealthCourseVideoMapper healthCourseVideoMapper,
                                   LiveCourseCategoryMapper liveCourseCategoryMapper,
-                                  ExpertMapper expertMapper) {
+                                  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;
     }
 
     /**
@@ -107,12 +155,128 @@
         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;
+    }
+
+    /**
+     * 收藏 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;
     }
 
     /**
@@ -150,6 +314,20 @@
             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));
     }
 
     /**
@@ -214,7 +392,7 @@
     /**
      * 将启用视频列表转换为 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();
         }
@@ -228,6 +406,13 @@
             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;
@@ -279,6 +464,337 @@
      */
     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<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 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;
     }
 
     /**
@@ -338,4 +854,63 @@
         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;
+        }
+    }
 }

--
Gitblit v1.7.1