package com.tcm.training.admin.live.service; import cn.idev.excel.FastExcel; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tcm.training.admin.common.Jsons; import com.tcm.training.admin.common.PageResponse; import com.tcm.training.admin.live.dto.HealthCourseSaveRequest; import com.tcm.training.admin.live.dto.HealthCourseStatusRequest; import com.tcm.training.admin.live.dto.HealthCourseVideoSaveRequest; import com.tcm.training.admin.live.entity.Expert; import com.tcm.training.admin.live.entity.HealthCourse; import com.tcm.training.admin.live.entity.HealthCourseVideo; import com.tcm.training.admin.live.entity.LiveCourseCategory; import com.tcm.training.admin.live.excel.HealthCourseExportRow; import com.tcm.training.admin.live.mapper.ExpertMapper; import com.tcm.training.admin.live.mapper.HealthCourseMapper; import com.tcm.training.admin.live.mapper.HealthCourseVideoMapper; import com.tcm.training.admin.live.mapper.LiveCourseCategoryMapper; import com.tcm.training.admin.live.vo.ExpertVO; import com.tcm.training.admin.live.vo.HealthCourseDetailRow; import com.tcm.training.admin.live.vo.HealthCourseDetailVO; import com.tcm.training.admin.live.vo.HealthCourseListItemVO; import com.tcm.training.admin.live.vo.HealthCourseListRow; import com.tcm.training.admin.live.vo.HealthCourseStatusVO; import com.tcm.training.admin.live.vo.HealthCourseVideoDetailVO; import com.tcm.training.admin.live.vo.HealthCourseVideoItemVO; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.math.BigDecimal; import java.net.URLEncoder; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Service public class HealthCourseAppService { private final HealthCourseMapper healthCourseMapper; private final HealthCourseVideoMapper healthCourseVideoMapper; private final LiveCourseCategoryMapper liveCourseCategoryMapper; private final ExpertMapper expertMapper; public HealthCourseAppService(HealthCourseMapper healthCourseMapper, HealthCourseVideoMapper healthCourseVideoMapper, LiveCourseCategoryMapper liveCourseCategoryMapper, ExpertMapper expertMapper) { this.healthCourseMapper = healthCourseMapper; this.healthCourseVideoMapper = healthCourseVideoMapper; this.liveCourseCategoryMapper = liveCourseCategoryMapper; this.expertMapper = expertMapper; } public PageResponse page(int page, int pageSize, String keyword, Long levelCategoryId, Long difficultyCategoryId, Integer status) { IPage result = healthCourseMapper.selectCoursePage( new Page(page, pageSize), trimToNull(keyword), levelCategoryId, difficultyCategoryId, status ); List list = new ArrayList(); for (HealthCourseListRow row : result.getRecords()) { list.add(toCourseListItem(row)); } return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize()); } public HealthCourseDetailVO detail(Long id) { HealthCourseDetailRow row = healthCourseMapper.selectCourseDetail(id); if (row == null) { throw new IllegalArgumentException("课程不存在"); } return toCourseDetail(row); } @Transactional public HealthCourseDetailVO create(HealthCourseSaveRequest req) { validateCategories(req.getLevelCategoryId(), req.getDifficultyCategoryId()); validateExpert(req.getExpertId()); HealthCourse course = new HealthCourse(); fillCourse(course, req); course.setRealSalesCount(0); course.setLearnUserCount(0); course.setFavoriteCount(0); course.setTotalDurationSeconds(0); course.setVideoCount(0); course.setSort(0); course.setDeleted(0); healthCourseMapper.insert(course); return detail(course.getId()); } @Transactional public HealthCourseDetailVO update(Long id, HealthCourseSaveRequest req) { validateCategories(req.getLevelCategoryId(), req.getDifficultyCategoryId()); validateExpert(req.getExpertId()); HealthCourse course = requireCourse(id); fillCourse(course, req); healthCourseMapper.updateById(course); return detail(id); } @Transactional public HealthCourseStatusVO patchStatus(Long id, HealthCourseStatusRequest req) { HealthCourse course = requireCourse(id); course.setStatus(req.getStatus()); healthCourseMapper.updateById(course); return new HealthCourseStatusVO(id, req.getStatus()); } @Transactional public void delete(Long id) { requireCourse(id); List videos = healthCourseVideoMapper.selectList(new LambdaQueryWrapper() .eq(HealthCourseVideo::getCourseId, id) .eq(HealthCourseVideo::getDeleted, 0)); for (HealthCourseVideo video : videos) { healthCourseVideoMapper.deleteById(video.getId()); } healthCourseMapper.deleteById(id); } public void export(HttpServletResponse response, String keyword, Long levelCategoryId, Long difficultyCategoryId, Integer status) throws IOException { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String filename = URLEncoder.encode("养身课程", "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx"); List rows = healthCourseMapper.selectCourseList( trimToNull(keyword), levelCategoryId, difficultyCategoryId, status ); List exports = new ArrayList(); for (int i = 0; i < rows.size(); i++) { HealthCourseListRow row = rows.get(i); HealthCourseExportRow export = new HealthCourseExportRow(); export.setSerialNo(i + 1); export.setTitle(defaultText(row.getTitle())); export.setLevelName(defaultText(row.getLevelName())); export.setDifficultyName(defaultText(row.getDifficultyName())); export.setSalePrice(row.getSalePrice() == null ? BigDecimal.ZERO : row.getSalePrice()); export.setSalesCount(resolveSalesCount(row.getBaseSalesCount(), row.getRealSalesCount())); export.setLearnUserCount(defaultNumber(row.getLearnUserCount())); export.setTotalDuration(formatDuration(defaultNumber(row.getTotalDurationSeconds()))); export.setFavoriteCount(defaultNumber(row.getFavoriteCount())); export.setExpertName(defaultText(row.getExpertName())); export.setStatusText(statusText(row.getStatus())); exports.add(export); } FastExcel.write(response.getOutputStream(), HealthCourseExportRow.class) .sheet("养身课程") .doWrite(exports); } public PageResponse pageVideos(Long courseId, int page, int pageSize, String chapterName, Integer saleType) { requireCourse(courseId); LambdaQueryWrapper qw = new LambdaQueryWrapper() .eq(HealthCourseVideo::getCourseId, courseId) .eq(HealthCourseVideo::getDeleted, 0) .orderByAsc(HealthCourseVideo::getSort) .orderByAsc(HealthCourseVideo::getId); if (chapterName != null && !chapterName.trim().isEmpty()) { qw.like(HealthCourseVideo::getChapterName, chapterName.trim()); } if (saleType != null) { qw.eq(HealthCourseVideo::getSaleType, saleType); } IPage result = healthCourseVideoMapper.selectPage(new Page(page, pageSize), qw); List list = new ArrayList(); for (HealthCourseVideo item : result.getRecords()) { list.add(toVideoItem(item)); } return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize()); } public HealthCourseVideoDetailVO detailVideo(Long courseId, Long videoId) { return toVideoDetail(requireVideo(courseId, videoId)); } @Transactional public HealthCourseVideoDetailVO createVideo(Long courseId, HealthCourseVideoSaveRequest req) { requireCourse(courseId); validateVideoSale(req); HealthCourseVideo next = new HealthCourseVideo(); next.setCourseId(courseId); fillVideo(next, req); next.setSort(resolveNextVideoSort(courseId)); next.setDeleted(0); healthCourseVideoMapper.insert(next); refreshCourseStats(courseId); return detailVideo(courseId, next.getId()); } @Transactional public HealthCourseVideoDetailVO updateVideo(Long courseId, Long videoId, HealthCourseVideoSaveRequest req) { validateVideoSale(req); HealthCourseVideo video = requireVideo(courseId, videoId); fillVideo(video, req); healthCourseVideoMapper.updateById(video); refreshCourseStats(courseId); return detailVideo(courseId, videoId); } @Transactional public void deleteVideo(Long courseId, Long videoId) { requireVideo(courseId, videoId); healthCourseVideoMapper.deleteById(videoId); refreshCourseStats(courseId); } private void fillCourse(HealthCourse course, HealthCourseSaveRequest req) { course.setTitle(req.getTitle().trim()); course.setLevelCategoryId(req.getLevelCategoryId()); course.setDifficultyCategoryId(req.getDifficultyCategoryId()); course.setExpertId(req.getExpertId()); course.setSalePrice(req.getSalePrice()); course.setSummary(trimToNull(req.getSummary())); course.setTargetUsers(trimToNull(req.getTargetUsers())); course.setCoverImg(req.getCoverImg().trim()); course.setBannerImgs(req.getBannerImgs()); course.setContent(req.getContent().trim()); course.setBaseSalesCount(req.getBaseSalesCount() == null ? 0 : req.getBaseSalesCount()); course.setStatus(req.getStatus()); course.setIsHomeRecommend(req.getIsHomeRecommend()); } private void fillVideo(HealthCourseVideo video, HealthCourseVideoSaveRequest req) { video.setChapterName(req.getChapterName().trim()); video.setChapterRemark(trimToNull(req.getChapterRemark())); video.setVideoUrl(req.getVideoUrl().trim()); video.setDurationSeconds(req.getDurationSeconds()); video.setSaleType(req.getSaleType()); video.setSalePrice(req.getSaleType() != null && req.getSaleType() == 0 ? BigDecimal.ZERO : req.getSalePrice()); video.setPlayCount(req.getPlayCount()); video.setStatus(req.getStatus()); } private void validateCategories(Long levelCategoryId, Long difficultyCategoryId) { LiveCourseCategory level = requireCategory(levelCategoryId, LiveCourseCategoryAppService.TYPE_LEVEL, "养身等级"); LiveCourseCategory difficulty = requireCategory(difficultyCategoryId, LiveCourseCategoryAppService.TYPE_DIFFICULTY, "难度"); if (level == null || difficulty == null) { throw new IllegalArgumentException("分类不存在"); } } private LiveCourseCategory requireCategory(Long id, String type, String label) { LiveCourseCategory category = liveCourseCategoryMapper.selectOne(new LambdaQueryWrapper() .eq(LiveCourseCategory::getId, id) .eq(LiveCourseCategory::getType, type) .eq(LiveCourseCategory::getDeleted, 0) .last("limit 1")); if (category == null) { throw new IllegalArgumentException(label + "不存在"); } return category; } private void validateExpert(Long expertId) { Expert expert = expertMapper.selectOne(new LambdaQueryWrapper() .eq(Expert::getId, expertId) .eq(Expert::getDeleted, 0) .last("limit 1")); if (expert == null) { throw new IllegalArgumentException("授课专家不存在"); } if (expert.getStatus() == null || expert.getStatus() != 1) { throw new IllegalArgumentException("授课专家已禁用"); } } private void validateVideoSale(HealthCourseVideoSaveRequest req) { if (req.getSaleType() == null || (req.getSaleType() != 0 && req.getSaleType() != 1)) { throw new IllegalArgumentException("售价类型不合法"); } if (req.getSaleType() == 0 && req.getSalePrice() != null && req.getSalePrice().compareTo(BigDecimal.ZERO) != 0) { throw new IllegalArgumentException("免费课程售价必须为0"); } } private int resolveNextVideoSort(Long courseId) { HealthCourseVideo latest = healthCourseVideoMapper.selectOne(new LambdaQueryWrapper() .eq(HealthCourseVideo::getCourseId, courseId) .eq(HealthCourseVideo::getDeleted, 0) .orderByDesc(HealthCourseVideo::getSort) .last("limit 1")); if (latest == null || latest.getSort() == null) { return 10; } return latest.getSort() + 10; } private void refreshCourseStats(Long courseId) { HealthCourse course = requireCourse(courseId); List videos = healthCourseVideoMapper.selectList(new LambdaQueryWrapper() .eq(HealthCourseVideo::getCourseId, courseId) .eq(HealthCourseVideo::getDeleted, 0)); int totalDurationSeconds = 0; for (HealthCourseVideo video : videos) { totalDurationSeconds += video.getDurationSeconds() == null ? 0 : video.getDurationSeconds(); } course.setVideoCount(videos.size()); course.setTotalDurationSeconds(totalDurationSeconds); healthCourseMapper.updateById(course); } private HealthCourse requireCourse(Long id) { HealthCourse course = healthCourseMapper.selectById(id); if (course == null) { throw new IllegalArgumentException("课程不存在"); } return course; } private HealthCourseVideo requireVideo(Long courseId, Long videoId) { requireCourse(courseId); HealthCourseVideo video = healthCourseVideoMapper.selectById(videoId); if (video == null) { throw new IllegalArgumentException("视频课程不存在"); } if (!courseId.equals(video.getCourseId())) { throw new IllegalArgumentException("视频课程不属于当前课程"); } return video; } private HealthCourseListItemVO toCourseListItem(HealthCourseListRow row) { HealthCourseListItemVO vo = new HealthCourseListItemVO(); vo.setId(row.getId()); vo.setTitle(row.getTitle()); vo.setLevelName(row.getLevelName()); vo.setDifficultyName(row.getDifficultyName()); vo.setSalePrice(row.getSalePrice() == null ? BigDecimal.ZERO : row.getSalePrice()); vo.setSalesCount(resolveSalesCount(row.getBaseSalesCount(), row.getRealSalesCount())); vo.setLearnUserCount(defaultNumber(row.getLearnUserCount())); vo.setTotalDurationSeconds(defaultNumber(row.getTotalDurationSeconds())); vo.setFavoriteCount(defaultNumber(row.getFavoriteCount())); vo.setExpert(new ExpertVO(row.getExpertId(), row.getExpertName(), row.getExpertAvatar())); vo.setCoverImg(row.getCoverImg()); vo.setStatus(row.getStatus()); vo.setIsHomeRecommend(row.getIsHomeRecommend()); vo.setCreatedAt(row.getCreatedAt()); return vo; } private HealthCourseDetailVO toCourseDetail(HealthCourseDetailRow row) { HealthCourseDetailVO vo = new HealthCourseDetailVO(); vo.setId(row.getId()); vo.setTitle(row.getTitle()); vo.setLevelCategoryId(row.getLevelCategoryId()); vo.setLevelName(row.getLevelName()); vo.setDifficultyCategoryId(row.getDifficultyCategoryId()); vo.setDifficultyName(row.getDifficultyName()); vo.setExpert(new ExpertVO(row.getExpertId(), row.getExpertName(), row.getExpertAvatar())); vo.setSalePrice(row.getSalePrice() == null ? BigDecimal.ZERO : row.getSalePrice()); vo.setSummary(row.getSummary()); vo.setTargetUsers(row.getTargetUsers()); vo.setCoverImg(row.getCoverImg()); vo.setBannerImgs(Jsons.parseStringList(row.getBannerImgs())); vo.setContent(row.getContent()); vo.setBaseSalesCount(defaultNumber(row.getBaseSalesCount())); vo.setRealSalesCount(defaultNumber(row.getRealSalesCount())); vo.setSalesCount(resolveSalesCount(row.getBaseSalesCount(), row.getRealSalesCount())); vo.setLearnUserCount(defaultNumber(row.getLearnUserCount())); vo.setFavoriteCount(defaultNumber(row.getFavoriteCount())); vo.setTotalDurationSeconds(defaultNumber(row.getTotalDurationSeconds())); vo.setVideoCount(defaultNumber(row.getVideoCount())); vo.setStatus(row.getStatus()); vo.setIsHomeRecommend(row.getIsHomeRecommend()); vo.setSort(row.getSort()); vo.setCreatedAt(row.getCreatedAt()); vo.setUpdatedAt(row.getUpdatedAt()); return vo; } private HealthCourseVideoItemVO toVideoItem(HealthCourseVideo item) { HealthCourseVideoItemVO vo = new HealthCourseVideoItemVO(); vo.setId(item.getId()); vo.setCourseId(item.getCourseId()); vo.setChapterName(item.getChapterName()); vo.setChapterRemark(item.getChapterRemark()); vo.setVideoUrl(item.getVideoUrl()); vo.setDurationSeconds(item.getDurationSeconds()); vo.setSaleType(item.getSaleType()); vo.setSalePrice(item.getSalePrice()); vo.setPlayCount(item.getPlayCount()); vo.setSort(item.getSort()); vo.setStatus(item.getStatus()); vo.setCreatedAt(item.getCreatedAt()); return vo; } private HealthCourseVideoDetailVO toVideoDetail(HealthCourseVideo item) { HealthCourseVideoDetailVO vo = new HealthCourseVideoDetailVO(); vo.setId(item.getId()); vo.setCourseId(item.getCourseId()); vo.setChapterName(item.getChapterName()); vo.setChapterRemark(item.getChapterRemark()); vo.setVideoUrl(item.getVideoUrl()); vo.setDurationSeconds(item.getDurationSeconds()); vo.setSaleType(item.getSaleType()); vo.setSalePrice(item.getSalePrice()); vo.setPlayCount(item.getPlayCount()); vo.setSort(item.getSort()); vo.setStatus(item.getStatus()); vo.setCreatedAt(item.getCreatedAt()); vo.setUpdatedAt(item.getUpdatedAt()); return vo; } private Integer resolveSalesCount(Integer baseSalesCount, Integer realSalesCount) { return defaultNumber(baseSalesCount) + defaultNumber(realSalesCount); } private Integer defaultNumber(Integer value) { return value == null ? 0 : value; } private String formatDuration(Integer seconds) { int totalSeconds = seconds == null ? 0 : seconds; int minutes = totalSeconds / 60; int remainSeconds = totalSeconds % 60; return minutes + ":" + String.format("%02d", remainSeconds); } private String statusText(Integer status) { return status != null && status == 1 ? "启用" : "禁用"; } private String defaultText(String text) { return text == null || text.trim().isEmpty() ? "-" : text; } private String trimToNull(String text) { if (text == null) { return null; } String trimmed = text.trim(); return trimmed.isEmpty() ? null : trimmed; } }