| | |
| | | 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 养生课程详情聚合服务。 |
| | |
| | | |
| | | 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 养生课程详情所需依赖。 |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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)); |
| | | } |
| | | |
| | | /** |
| | |
| | | /** |
| | | * 将启用视频列表转换为 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())); |
| | | 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 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |