From ba3233a5b685f0ae6856d8dc5a2bf904296cf8b7 Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期四, 26 三月 2026 15:08:46 +0800
Subject: [PATCH] feat:交易与流水-订单-会员购买订单、交易与流水-订单-养生课程购买订单、交易与流水-财务流水-支付流水、交易与流水-财务流水-退费流水、课程管理-健康科普

---
 backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java |  381 ++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 337 insertions(+), 44 deletions(-)

diff --git a/backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java b/backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java
index 209a499..5a9d311 100644
--- a/backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java
+++ b/backend/src/main/java/com/tcm/training/admin/live/service/HealthCourseAppService.java
@@ -14,6 +14,7 @@
 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.excel.HealthCourseVideoImportRow;
 import com.tcm.training.admin.live.mapper.ExpertMapper;
 import com.tcm.training.admin.live.mapper.HealthCourseMapper;
 import com.tcm.training.admin.live.mapper.HealthCourseVideoMapper;
@@ -23,11 +24,13 @@
 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.HealthCourseOptionVO;
 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 org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
@@ -35,7 +38,13 @@
 import java.net.URLEncoder;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @Service
 public class HealthCourseAppService {
@@ -138,7 +147,7 @@
                        Integer status) throws IOException {
         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
         response.setCharacterEncoding("utf-8");
-        String filename = URLEncoder.encode("养身课程", "UTF-8").replace("+", "%20");
+        String filename = URLEncoder.encode("养生课程", "UTF-8").replace("+", "%20");
         response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
 
         List<HealthCourseListRow> rows = healthCourseMapper.selectCourseList(
@@ -165,15 +174,28 @@
             exports.add(export);
         }
         FastExcel.write(response.getOutputStream(), HealthCourseExportRow.class)
-                .sheet("养身课程")
+                .sheet("养生课程")
                 .doWrite(exports);
+    }
+
+    public List<HealthCourseOptionVO> options(String keyword) {
+        List<HealthCourse> courses = healthCourseMapper.selectList(new LambdaQueryWrapper<HealthCourse>()
+                .eq(HealthCourse::getDeleted, 0)
+                .like(keyword != null && !keyword.trim().isEmpty(), HealthCourse::getTitle, keyword == null ? null : keyword.trim())
+                .orderByAsc(HealthCourse::getSort)
+                .orderByDesc(HealthCourse::getId));
+        List<HealthCourseOptionVO> list = new ArrayList<HealthCourseOptionVO>();
+        for (HealthCourse course : courses) {
+            list.add(new HealthCourseOptionVO(course.getId(), course.getTitle()));
+        }
+        return list;
     }
 
     public PageResponse<HealthCourseVideoItemVO> pageVideos(Long courseId,
                                                             int page,
                                                             int pageSize,
                                                             String chapterName,
-                                                            Integer saleType) {
+                                                            Integer priceType) {
         requireCourse(courseId);
         LambdaQueryWrapper<HealthCourseVideo> qw = new LambdaQueryWrapper<HealthCourseVideo>()
                 .eq(HealthCourseVideo::getCourseId, courseId)
@@ -183,13 +205,55 @@
         if (chapterName != null && !chapterName.trim().isEmpty()) {
             qw.like(HealthCourseVideo::getChapterName, chapterName.trim());
         }
-        if (saleType != null) {
-            qw.eq(HealthCourseVideo::getSaleType, saleType);
+        if (priceType != null) {
+            if (priceType == 0) {
+                qw.eq(HealthCourseVideo::getSalePrice, BigDecimal.ZERO);
+            } else if (priceType == 1) {
+                qw.gt(HealthCourseVideo::getSalePrice, BigDecimal.ZERO);
+            }
         }
         IPage<HealthCourseVideo> result = healthCourseVideoMapper.selectPage(new Page<HealthCourseVideo>(page, pageSize), qw);
         List<HealthCourseVideoItemVO> list = new ArrayList<HealthCourseVideoItemVO>();
         for (HealthCourseVideo item : result.getRecords()) {
             list.add(toVideoItem(item));
+        }
+        return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    public PageResponse<HealthCourseVideoItemVO> pageVideoResources(int page,
+                                                                    int pageSize,
+                                                                    String courseKeyword,
+                                                                    Long courseId,
+                                                                    Integer courseType,
+                                                                    String chapterName,
+                                                                    Integer priceType,
+                                                                    Integer status) {
+        List<Long> matchedCourseIds = resolveMatchedCourseIds(courseKeyword, courseId);
+        if (matchedCourseIds.isEmpty()) {
+            return PageResponse.of(Collections.<HealthCourseVideoItemVO>emptyList(), 0, page, pageSize);
+        }
+        LambdaQueryWrapper<HealthCourseVideo> qw = new LambdaQueryWrapper<HealthCourseVideo>()
+                .in(HealthCourseVideo::getCourseId, matchedCourseIds)
+                .eq(HealthCourseVideo::getDeleted, 0)
+                .eq(courseType != null, HealthCourseVideo::getCourseType, courseType)
+                .eq(status != null, HealthCourseVideo::getStatus, status)
+                .orderByAsc(HealthCourseVideo::getSort)
+                .orderByDesc(HealthCourseVideo::getId);
+        if (chapterName != null && !chapterName.trim().isEmpty()) {
+            qw.like(HealthCourseVideo::getChapterName, chapterName.trim());
+        }
+        if (priceType != null) {
+            if (priceType == 0) {
+                qw.eq(HealthCourseVideo::getSalePrice, BigDecimal.ZERO);
+            } else if (priceType == 1) {
+                qw.gt(HealthCourseVideo::getSalePrice, BigDecimal.ZERO);
+            }
+        }
+        IPage<HealthCourseVideo> result = healthCourseVideoMapper.selectPage(new Page<HealthCourseVideo>(page, pageSize), qw);
+        Map<Long, String> courseTitleMap = resolveCourseTitleMap(extractCourseIds(result.getRecords()));
+        List<HealthCourseVideoItemVO> list = new ArrayList<HealthCourseVideoItemVO>();
+        for (HealthCourseVideo item : result.getRecords()) {
+            list.add(toVideoItem(item, courseTitleMap.get(item.getCourseId())));
         }
         return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
     }
@@ -200,26 +264,14 @@
 
     @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());
+        req.setCourseId(courseId);
+        return createVideoResource(req);
     }
 
     @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);
+        req.setCourseId(courseId);
+        return updateVideoResource(videoId, req);
     }
 
     @Transactional
@@ -227,6 +279,114 @@
         requireVideo(courseId, videoId);
         healthCourseVideoMapper.deleteById(videoId);
         refreshCourseStats(courseId);
+    }
+
+    public HealthCourseVideoDetailVO detailVideoResource(Long id) {
+        HealthCourseVideo video = requireVideoById(id);
+        return toVideoDetail(video, requireCourse(video.getCourseId()).getTitle());
+    }
+
+    @Transactional
+    public HealthCourseVideoDetailVO createVideoResource(HealthCourseVideoSaveRequest req) {
+        Long courseId = requireVideoCourseId(req.getCourseId());
+        validateVideo(req);
+        HealthCourseVideo next = new HealthCourseVideo();
+        next.setCourseId(courseId);
+        fillVideo(next, req);
+        next.setSort(resolveNextVideoSort(courseId));
+        next.setDeleted(0);
+        healthCourseVideoMapper.insert(next);
+        refreshCourseStats(courseId);
+        return detailVideoResource(next.getId());
+    }
+
+    @Transactional
+    public HealthCourseVideoDetailVO updateVideoResource(Long id, HealthCourseVideoSaveRequest req) {
+        HealthCourseVideo video = requireVideoById(id);
+        Long targetCourseId = req.getCourseId() == null ? video.getCourseId() : requireVideoCourseId(req.getCourseId());
+        req.setCourseId(targetCourseId);
+        validateVideo(req);
+        Long oldCourseId = video.getCourseId();
+        video.setCourseId(targetCourseId);
+        fillVideo(video, req);
+        healthCourseVideoMapper.updateById(video);
+        refreshCourseStats(targetCourseId);
+        if (!oldCourseId.equals(targetCourseId)) {
+            refreshCourseStats(oldCourseId);
+        }
+        return detailVideoResource(id);
+    }
+
+    @Transactional
+    public void deleteVideoResource(Long id) {
+        HealthCourseVideo video = requireVideoById(id);
+        healthCourseVideoMapper.deleteById(id);
+        refreshCourseStats(video.getCourseId());
+    }
+
+    public void downloadVideoResourceTemplate(HttpServletResponse response) 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<HealthCourseVideoImportRow> rows = new ArrayList<HealthCourseVideoImportRow>();
+        HealthCourseVideoImportRow sample = new HealthCourseVideoImportRow();
+        sample.setCourseId(1L);
+        sample.setCourseTitle("中医针灸资格证");
+        sample.setCourseTypeText(courseTypeText(HealthCourseVideo.COURSE_TYPE_HEALTH));
+        sample.setChapterName("方剂学");
+        sample.setSalePrice(BigDecimal.ZERO);
+        sample.setDurationSeconds(238);
+        sample.setVideoUrl("https://example.com/video.mp4");
+        sample.setStatusText(statusText(1));
+        rows.add(sample);
+        FastExcel.write(response.getOutputStream(), HealthCourseVideoImportRow.class)
+                .sheet("视频资源导入模板")
+                .doWrite(rows);
+    }
+
+    @Transactional
+    public void importVideoResources(MultipartFile file) throws IOException {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("请选择导入文件");
+        }
+        List<HealthCourseVideoImportRow> rows = FastExcel.read(file.getInputStream())
+                .head(HealthCourseVideoImportRow.class)
+                .sheet()
+                .doReadSync();
+        if (rows == null || rows.isEmpty()) {
+            throw new IllegalArgumentException("导入文件不能为空");
+        }
+        Set<Long> touchedCourseIds = new LinkedHashSet<Long>();
+        int rowNum = 1;
+        for (HealthCourseVideoImportRow row : rows) {
+            rowNum++;
+            if (row == null || isImportRowEmpty(row)) {
+                continue;
+            }
+            validateImportRow(row, rowNum);
+            HealthCourse course = requireCourse(row.getCourseId());
+            HealthCourseVideo video = new HealthCourseVideo();
+            video.setCourseId(course.getId());
+            video.setCourseType(HealthCourseVideo.COURSE_TYPE_HEALTH);
+            video.setChapterName(row.getChapterName().trim());
+            video.setVideoSourceType(HealthCourseVideo.VIDEO_SOURCE_URL);
+            video.setVideoUrl(row.getVideoUrl().trim());
+            video.setVideoFileName(null);
+            video.setDurationSeconds(row.getDurationSeconds());
+            video.setSalePrice(row.getSalePrice() == null ? BigDecimal.ZERO : row.getSalePrice());
+            video.setSort(resolveNextVideoSort(course.getId()));
+            video.setStatus(parseStatusText(row.getStatusText()));
+            video.setDeleted(0);
+            healthCourseVideoMapper.insert(video);
+            touchedCourseIds.add(course.getId());
+        }
+        if (touchedCourseIds.isEmpty()) {
+            throw new IllegalArgumentException("导入文件没有可用数据");
+        }
+        for (Long courseId : touchedCourseIds) {
+            refreshCourseStats(courseId);
+        }
     }
 
     private void fillCourse(HealthCourse course, HealthCourseSaveRequest req) {
@@ -246,18 +406,18 @@
     }
 
     private void fillVideo(HealthCourseVideo video, HealthCourseVideoSaveRequest req) {
+        video.setCourseType(resolveCourseType(req.getCourseType()));
         video.setChapterName(req.getChapterName().trim());
-        video.setChapterRemark(trimToNull(req.getChapterRemark()));
+        video.setVideoSourceType(req.getVideoSourceType());
         video.setVideoUrl(req.getVideoUrl().trim());
+        video.setVideoFileName(trimToNull(req.getVideoFileName()));
         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());
+        video.setSalePrice(req.getSalePrice());
+        video.setStatus(resolveVideoStatus(req.getStatus()));
     }
 
     private void validateCategories(Long levelCategoryId, Long difficultyCategoryId) {
-        LiveCourseCategory level = requireCategory(levelCategoryId, LiveCourseCategoryAppService.TYPE_LEVEL, "养身等级");
+        LiveCourseCategory level = requireCategory(levelCategoryId, LiveCourseCategoryAppService.TYPE_LEVEL, "养生等级");
         LiveCourseCategory difficulty = requireCategory(difficultyCategoryId, LiveCourseCategoryAppService.TYPE_DIFFICULTY, "难度");
         if (level == null || difficulty == null) {
             throw new IllegalArgumentException("分类不存在");
@@ -289,13 +449,29 @@
         }
     }
 
-    private void validateVideoSale(HealthCourseVideoSaveRequest req) {
-        if (req.getSaleType() == null || (req.getSaleType() != 0 && req.getSaleType() != 1)) {
-            throw new IllegalArgumentException("售价类型不合法");
+    private void validateVideo(HealthCourseVideoSaveRequest req) {
+        Integer courseType = resolveCourseType(req.getCourseType());
+        if (courseType != HealthCourseVideo.COURSE_TYPE_HEALTH) {
+            throw new IllegalArgumentException("课程类型不合法");
         }
-        if (req.getSaleType() == 0 && req.getSalePrice() != null && req.getSalePrice().compareTo(BigDecimal.ZERO) != 0) {
-            throw new IllegalArgumentException("免费课程售价必须为0");
+        if (req.getCourseId() == null) {
+            throw new IllegalArgumentException("请选择关联课程");
         }
+        if (req.getVideoSourceType() == null
+                || (req.getVideoSourceType() != HealthCourseVideo.VIDEO_SOURCE_URL
+                && req.getVideoSourceType() != HealthCourseVideo.VIDEO_SOURCE_UPLOAD)) {
+            throw new IllegalArgumentException("视频来源不合法");
+        }
+        if (req.getVideoUrl() == null || req.getVideoUrl().trim().isEmpty()) {
+            throw new IllegalArgumentException("请输入视频地址");
+        }
+        if (req.getSalePrice() != null && req.getSalePrice().compareTo(BigDecimal.ZERO) < 0) {
+            throw new IllegalArgumentException("售价不能小于0");
+        }
+    }
+
+    private Integer resolveVideoStatus(Integer status) {
+        return status == null ? 1 : status;
     }
 
     private int resolveNextVideoSort(Long courseId) {
@@ -334,10 +510,7 @@
 
     private HealthCourseVideo requireVideo(Long courseId, Long videoId) {
         requireCourse(courseId);
-        HealthCourseVideo video = healthCourseVideoMapper.selectById(videoId);
-        if (video == null) {
-            throw new IllegalArgumentException("视频课程不存在");
-        }
+        HealthCourseVideo video = requireVideoById(videoId);
         if (!courseId.equals(video.getCourseId())) {
             throw new IllegalArgumentException("视频课程不属于当前课程");
         }
@@ -394,33 +567,49 @@
     }
 
     private HealthCourseVideoItemVO toVideoItem(HealthCourseVideo item) {
+        return toVideoItem(item, null);
+    }
+
+    private HealthCourseVideoItemVO toVideoItem(HealthCourseVideo item, String courseTitle) {
         HealthCourseVideoItemVO vo = new HealthCourseVideoItemVO();
         vo.setId(item.getId());
+        vo.setCourseType(resolveCourseType(item.getCourseType()));
+        vo.setCourseTypeText(courseTypeText(item.getCourseType()));
         vo.setCourseId(item.getCourseId());
+        vo.setCourseTitle(defaultText(courseTitle));
         vo.setChapterName(item.getChapterName());
-        vo.setChapterRemark(item.getChapterRemark());
+        vo.setVideoSourceType(item.getVideoSourceType());
         vo.setVideoUrl(item.getVideoUrl());
+        vo.setVideoFileName(item.getVideoFileName());
         vo.setDurationSeconds(item.getDurationSeconds());
-        vo.setSaleType(item.getSaleType());
-        vo.setSalePrice(item.getSalePrice());
-        vo.setPlayCount(item.getPlayCount());
+        vo.setDurationText(formatDuration(defaultNumber(item.getDurationSeconds())));
+        vo.setSalePrice(item.getSalePrice() == null ? BigDecimal.ZERO : item.getSalePrice());
+        vo.setWatchCount(0);
         vo.setSort(item.getSort());
         vo.setStatus(item.getStatus());
+        vo.setStatusText(statusText(item.getStatus()));
         vo.setCreatedAt(item.getCreatedAt());
         return vo;
     }
 
     private HealthCourseVideoDetailVO toVideoDetail(HealthCourseVideo item) {
+        return toVideoDetail(item, null);
+    }
+
+    private HealthCourseVideoDetailVO toVideoDetail(HealthCourseVideo item, String courseTitle) {
         HealthCourseVideoDetailVO vo = new HealthCourseVideoDetailVO();
         vo.setId(item.getId());
+        vo.setCourseType(resolveCourseType(item.getCourseType()));
+        vo.setCourseTypeText(courseTypeText(item.getCourseType()));
         vo.setCourseId(item.getCourseId());
+        vo.setCourseTitle(defaultText(courseTitle));
         vo.setChapterName(item.getChapterName());
-        vo.setChapterRemark(item.getChapterRemark());
+        vo.setVideoSourceType(item.getVideoSourceType());
         vo.setVideoUrl(item.getVideoUrl());
+        vo.setVideoFileName(item.getVideoFileName());
         vo.setDurationSeconds(item.getDurationSeconds());
-        vo.setSaleType(item.getSaleType());
-        vo.setSalePrice(item.getSalePrice());
-        vo.setPlayCount(item.getPlayCount());
+        vo.setSalePrice(item.getSalePrice() == null ? BigDecimal.ZERO : item.getSalePrice());
+        vo.setWatchCount(0);
         vo.setSort(item.getSort());
         vo.setStatus(item.getStatus());
         vo.setCreatedAt(item.getCreatedAt());
@@ -447,6 +636,10 @@
         return status != null && status == 1 ? "启用" : "禁用";
     }
 
+    private String courseTypeText(Integer courseType) {
+        return resolveCourseType(courseType) == HealthCourseVideo.COURSE_TYPE_HEALTH ? "养生课程" : "-";
+    }
+
     private String defaultText(String text) {
         return text == null || text.trim().isEmpty() ? "-" : text;
     }
@@ -458,4 +651,104 @@
         String trimmed = text.trim();
         return trimmed.isEmpty() ? null : trimmed;
     }
+
+    private Integer resolveCourseType(Integer courseType) {
+        return courseType == null ? HealthCourseVideo.COURSE_TYPE_HEALTH : courseType;
+    }
+
+    private Long requireVideoCourseId(Long courseId) {
+        return requireCourse(courseId).getId();
+    }
+
+    private HealthCourseVideo requireVideoById(Long id) {
+        HealthCourseVideo video = healthCourseVideoMapper.selectById(id);
+        if (video == null) {
+            throw new IllegalArgumentException("视频课程不存在");
+        }
+        return video;
+    }
+
+    private List<Long> resolveMatchedCourseIds(String courseKeyword, Long courseId) {
+        if (courseId != null) {
+            return Collections.singletonList(requireCourse(courseId).getId());
+        }
+        List<HealthCourse> courses = healthCourseMapper.selectList(new LambdaQueryWrapper<HealthCourse>()
+                .eq(HealthCourse::getDeleted, 0)
+                .like(courseKeyword != null && !courseKeyword.trim().isEmpty(), HealthCourse::getTitle, courseKeyword == null ? null : courseKeyword.trim()));
+        if (courses.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Long> ids = new ArrayList<Long>();
+        for (HealthCourse course : courses) {
+            ids.add(course.getId());
+        }
+        return ids;
+    }
+
+    private Map<Long, String> resolveCourseTitleMap(Collection<Long> courseIds) {
+        if (courseIds == null || courseIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<HealthCourse> courses = healthCourseMapper.selectList(new LambdaQueryWrapper<HealthCourse>()
+                .in(HealthCourse::getId, courseIds)
+                .eq(HealthCourse::getDeleted, 0));
+        Map<Long, String> map = new LinkedHashMap<Long, String>();
+        for (HealthCourse course : courses) {
+            map.put(course.getId(), course.getTitle());
+        }
+        return map;
+    }
+
+    private List<Long> extractCourseIds(List<HealthCourseVideo> videos) {
+        Set<Long> ids = new LinkedHashSet<Long>();
+        for (HealthCourseVideo video : videos) {
+            ids.add(video.getCourseId());
+        }
+        return new ArrayList<Long>(ids);
+    }
+
+    private boolean isImportRowEmpty(HealthCourseVideoImportRow row) {
+        return row.getCourseId() == null
+                && (row.getChapterName() == null || row.getChapterName().trim().isEmpty())
+                && row.getSalePrice() == null
+                && row.getDurationSeconds() == null
+                && (row.getVideoUrl() == null || row.getVideoUrl().trim().isEmpty())
+                && (row.getStatusText() == null || row.getStatusText().trim().isEmpty());
+    }
+
+    private void validateImportRow(HealthCourseVideoImportRow row, int rowNum) {
+        if (row.getCourseId() == null) {
+            throw new IllegalArgumentException("第" + rowNum + "行:关联课程ID不能为空");
+        }
+        if (row.getCourseTypeText() != null && !row.getCourseTypeText().trim().isEmpty() && !"养生课程".equals(row.getCourseTypeText().trim())) {
+            throw new IllegalArgumentException("第" + rowNum + "行:课程类型仅支持养生课程");
+        }
+        if (row.getChapterName() == null || row.getChapterName().trim().isEmpty()) {
+            throw new IllegalArgumentException("第" + rowNum + "行:小节名称不能为空");
+        }
+        if (row.getSalePrice() != null && row.getSalePrice().compareTo(BigDecimal.ZERO) < 0) {
+            throw new IllegalArgumentException("第" + rowNum + "行:小节价格不能小于0");
+        }
+        if (row.getDurationSeconds() == null || row.getDurationSeconds() < 0) {
+            throw new IllegalArgumentException("第" + rowNum + "行:时长必须大于等于0");
+        }
+        if (row.getVideoUrl() == null || row.getVideoUrl().trim().isEmpty()) {
+            throw new IllegalArgumentException("第" + rowNum + "行:视频地址不能为空");
+        }
+        try {
+            parseStatusText(row.getStatusText());
+        } catch (IllegalArgumentException ex) {
+            throw new IllegalArgumentException("第" + rowNum + "行:" + ex.getMessage());
+        }
+    }
+
+    private Integer parseStatusText(String statusText) {
+        if (statusText == null || statusText.trim().isEmpty() || "启用".equals(statusText.trim())) {
+            return 1;
+        }
+        if ("禁用".equals(statusText.trim())) {
+            return 0;
+        }
+        throw new IllegalArgumentException("状态仅支持启用或禁用");
+    }
 }

--
Gitblit v1.7.1