diff --git a/servers/boe-server-all/src/main/java/com/xboe/UrlSecurityFilterImpl.java b/servers/boe-server-all/src/main/java/com/xboe/UrlSecurityFilterImpl.java index 0bbb4583..35243542 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/UrlSecurityFilterImpl.java +++ b/servers/boe-server-all/src/main/java/com/xboe/UrlSecurityFilterImpl.java @@ -35,6 +35,16 @@ public class UrlSecurityFilterImpl implements IUrlSecurityFilter{ noLoginUrls.add("/xboe/m/course/manage/test"); noLoginUrls.add("/xboe/m/course/manage/redirectDetail"); + + noLoginUrls.add("/xboe/account/update-avatar"); + + // 全量测试 + noLoginUrls.add("/xboe/m/course/content/save"); + + // 新的测试接口 + noLoginUrls.add("/xboe/m/course/content/courseware/save"); + noLoginUrls.add("/xboe/m/course/content/courseware/atomic-upload"); + noLoginUrls.add("/xboe/m/course/content/courseware/hierarchical-upload"); } @Override diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/api/CourseContentApi.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/api/CourseContentApi.java index 198311ca..23849f69 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/course/api/CourseContentApi.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/api/CourseContentApi.java @@ -1,24 +1,30 @@ package com.xboe.module.course.api; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.HashMap; -import javax.annotation.Resource; - +import javax.annotation.Resource;import com.xboe.module.course.dao.CourseDao; +import com.xboe.module.course.dto.*; +import com.xboe.module.course.service.*; +import com.xboe.module.course.vo.TypeTreeVo; +import com.xboe.system.organization.vo.OrganizationVo; +import org.apache.commons.collections4.CollectionUtils; +import com.xboe.common.OrderCondition; import com.xboe.common.utils.StringUtil; import com.xboe.core.log.AutoLog; +import com.xboe.core.orm.FieldFilters; +import com.xboe.module.course.dao.CourseContentDao; +import com.xboe.module.course.dao.CourseSectionDao; +import com.xboe.module.course.entity.*; import org.apache.commons.lang3.StringUtils; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import com.xboe.core.JsonResponse; import com.xboe.core.api.ApiBaseController; -import com.xboe.module.course.dto.CourseContentDto; -import com.xboe.module.course.dto.SortItem; -import com.xboe.module.course.entity.CourseAssess; -import com.xboe.module.course.entity.CourseExam; -import com.xboe.module.course.entity.CourseHomeWork; -import com.xboe.module.course.entity.CourseSection; -import com.xboe.module.course.service.ICourseContentService; -import com.xboe.module.course.service.ICourseSectionService; import com.xboe.standard.BaseConstant; import lombok.extern.slf4j.Slf4j; @@ -40,10 +46,31 @@ public class CourseContentApi extends ApiBaseController{ @Resource ICourseContentService ccontentService; - + @Resource + ICourseContentService courseWareService; + + @Resource + CourseSectionDao csectionDao; @Resource + TypeTreeService typeTreeService; + + @Resource + CourseDao courseDao; + + @Resource + private IAtomicBatchUploadService atomicBatchUploadService; + + @Resource + private CourseContentValidationService courseContentValidationService; + + + // 测试导入包 + @Resource + private CourseContentDao ccDao; + + /** * 获取课程的作业信息 - * @param cid + * @param ccid * @return */ @RequestMapping(value="/homework",method= {RequestMethod.POST,RequestMethod.GET}) @@ -159,7 +186,10 @@ public class CourseContentApi extends ApiBaseController{ return badRequest("无课程关联"); } try { - ccontentService.saveOrUpdate(cc); + // 测试 +// log.info("CourseContentDto: {}", cc); + + ccontentService.saveOrUpdate(cc); return success(cc); }catch(Exception e) { log.error("保存课程内容错误",e); @@ -167,6 +197,390 @@ public class CourseContentApi extends ApiBaseController{ } } + + /** + * 保存课件内容,同时把保存后的内容返回给前端,以便前端应用转化 + * @param cws + * @return + */ + @PostMapping("/courseware/save") + public JsonResponse saveCourseWare(@RequestBody CourseWareSaveDto cws) { + + // 1. 参数基础校验 + if (cws.getCourse() == null) { + return badRequest("课程基础信息不能为空"); + } + + // 课程名称必填校验 + if (StringUtils.isBlank(cws.getCourse().getName())) { + return badRequest("课程名称不能为空"); + } + + // 课程名称长度校验 + if (cws.getCourse().getName().length() > 100) { + return badRequest("课程名称长度不能超过100字"); + } + + if (CollectionUtils.isEmpty(cws.getTeacherList())) { + return badRequest("至少选择一位授课教师"); + } + + if (cws.getSysType1() == null || StringUtils.isBlank(cws.getSysType1().getId())) { + return badRequest("课程一级分类不能为空"); + } + + if (cws.getResOwner1() == null || StringUtils.isBlank(cws.getResOwner1().getId())) { + return badRequest("资源一级归属不能为空"); + } + + if (cws.getDevice() == null || (cws.getDevice() < 1 || cws.getDevice() > 3)) { + return badRequest("观看设置参数错误,必须是1、2或3"); + } + + // 2. 业务逻辑处理 + try { + // 记录操作日志 + String courseName = cws.getCourse().getId(); + log.info("开始保存/更新课程课件,课程ID:{}", courseName); + + // 调用ICourseContentService的saveOrUpdateCourseware方法 + ccontentService.saveOrUpdateCourseware(cws); + + // 操作成功,返回原DTO给前端 + log.info("课程课件保存/更新成功,课程ID:{}", courseName); + return success(cws, "保存成功"); + + } catch (Exception e) { + log.error("保存课件内容错误,课程名称:{}", cws.getCourse().getName(), e); + return error("保存失败:" + e.getMessage(), e.getMessage()); + } + } + + /** + * 信息批量上传(扁平化传参方式) + */ + @PostMapping("/courseware/atomic-upload") + public JsonResponse atomicBatchUpload( + @Validated @RequestBody BatchUploadWithNullDto uploadDto) { + + try { + log.info("接收批量上传请求,课程ID: {}, 操作类型: {}, 内容项数量: {}", + uploadDto.getCourseId(), + uploadDto.getOperationType(), + uploadDto.getContentItems() != null ? uploadDto.getContentItems().size() : 0); + + // 基础验证 + if (StringUtils.isBlank(uploadDto.getCourseId())) { + return badRequest("课程ID不能为空"); + } + + if (uploadDto.getOperationType() == null) { + return badRequest("操作类型不能为空"); + } + + if (uploadDto.getOperationType() != 1 ) { + return badRequest("操作类型必须是1(新增)"); + } + + if (CollectionUtils.isEmpty(uploadDto.getContentItems())) { + return badRequest("内容项列表不能为空"); + } + + // 验证每个内容项(使用服务层的验证方法) + List validationErrors = courseContentValidationService.validateBatchUpload(uploadDto); + if (CollectionUtils.isNotEmpty(validationErrors)) { + return badRequest(String.join("; ", validationErrors)); + } + + // 验证章节结构和资源唯一性 + List structureErrors = validateChapterSectionStructure(uploadDto); + if (CollectionUtils.isNotEmpty(structureErrors)) { + return badRequest(String.join("; ", structureErrors)); + } + + // 执行批量上传 + BatchUploadResponseDto response = atomicBatchUploadService.atomicBatchUpload(uploadDto); + + log.info("批量上传成功,课程ID: {}, 成功: {}, 失败: {}", + uploadDto.getCourseId(), response.getSuccessCount(), response.getFailCount()); + + return success(response, "原子批量上传成功"); + + } catch (RuntimeException e) { + log.error("原子批量上传失败", e); + return error("原子批量上传失败: " + e.getMessage()); + } catch (Exception e) { + log.error("系统异常", e); + return error("系统异常: " + e.getMessage()); + } + } + + /** + * 保存课程结构(课程->章->节->内容) + */ + @PostMapping("/courseware/hierarchical-upload") + public JsonResponse hierarchicalUpload( + @Validated @RequestBody HierarchicalCourseDto hierarchicalDto) { + + try { + log.info("接收层级结构上传请求,课程ID: {}, 章节数量: {}", + hierarchicalDto.getCourseId(), + hierarchicalDto.getChapters() != null ? hierarchicalDto.getChapters().size() : 0); + + // 层级结构->扁平结构 + BatchUploadWithNullDto batchDto = convertToBatchUploadDto(hierarchicalDto); + + // 基础验证 + if (StringUtils.isBlank(batchDto.getCourseId())) { + return badRequest("课程ID不能为空"); + } + + if (batchDto.getOperationType() == null) { + return badRequest("操作类型不能为空"); + } + + if (batchDto.getOperationType() != 1 ) { + return badRequest("操作类型必须是1(新增)"); + } + + if (CollectionUtils.isEmpty(batchDto.getContentItems())) { + return badRequest("内容项列表不能为空"); + } + + // 验证每个内容项 + List validationErrors = courseContentValidationService.validateBatchUpload(batchDto); + if (CollectionUtils.isNotEmpty(validationErrors)) { + return badRequest(String.join("; ", validationErrors)); + } + + // 验证章节结构和资源唯一性 + List structureErrors = validateChapterSectionStructure(batchDto); + if (CollectionUtils.isNotEmpty(structureErrors)) { + return badRequest(String.join("; ", structureErrors)); + } + + // 执行批量上传 + BatchUploadResponseDto response = atomicBatchUploadService.atomicBatchUpload(batchDto); + + log.info("层级结构上传成功,课程ID: {}, 成功: {}, 失败: {}", + batchDto.getCourseId(), response.getSuccessCount(), response.getFailCount()); + + return success(response, "层级结构上传成功"); + + } catch (RuntimeException e) { + log.error("层级结构上传失败", e); + return error("层级结构上传失败: " + e.getMessage()); + } catch (Exception e) { + log.error("系统异常", e); + return error("系统异常: " + e.getMessage()); + } + } + + /** + * 将层级结构DTO转换为扁平结构DTO + */ + private BatchUploadWithNullDto convertToBatchUploadDto(HierarchicalCourseDto hierarchicalDto) { + BatchUploadWithNullDto batchDto = new BatchUploadWithNullDto(); + batchDto.setCourseId(hierarchicalDto.getCourseId()); + batchDto.setOperationType(hierarchicalDto.getOperationType() != null ? + hierarchicalDto.getOperationType() : 1); // 默认为新增操作 + batchDto.setOwnerInfo(hierarchicalDto.getOwnerInfo()); + + List contentItems = new ArrayList<>(); + + // 遍历章节和节 + for (HierarchicalCourseDto.Chapter chapter : hierarchicalDto.getChapters()) { + for (HierarchicalCourseDto.Section section : chapter.getSections()) { + BatchUploadWithNullDto.ContentItem contentItem = section.getContent(); + contentItem.setCsectionId(chapter.getChapterId()); // 章ID + contentItems.add(contentItem); + } + } + + batchDto.setContentItems(contentItems); + return batchDto; + } + + /** + * 验证章节结构和资源唯一性 + * 确保: + * 1. 一个课程下面对应多个章 + * 2. 一个章底下对应多个节 + * 3. 一个节底下对应一个资源 + * 4. 每个节只能上传一种资源 + * + * @param uploadDto 批量上传DTO + * @return 验证错误列表 + */ + private List validateChapterSectionStructure(BatchUploadWithNullDto uploadDto) { + List errors = new ArrayList<>(); + + // 检查是否有重复的章节ID + Map> sectionContentMap = new HashMap<>(); + + for (int i = 0; i < uploadDto.getContentItems().size(); i++) { + BatchUploadWithNullDto.ContentItem item = uploadDto.getContentItems().get(i); + + // 检查是否指定章节ID + if (StringUtils.isBlank(item.getCsectionId())) { + errors.add("第" + (i + 1) + "项:必须指定章节ID"); + continue; + } + + // 将内容项按照章节ID分组 + sectionContentMap.computeIfAbsent(item.getCsectionId(), k -> new ArrayList<>()).add(item); + } + + // 检查每个章节是否只有一个资源 + for (Map.Entry> entry : sectionContentMap.entrySet()) { + String sectionId = entry.getKey(); + List itemsInSection = entry.getValue(); + + if (itemsInSection.size() > 1) { + errors.add("章节ID " + sectionId + " 下存在 " + itemsInSection.size() + " 个资源,每个章节只能有一个资源"); + } + } + + // 验证章节ID是否存在+属于当前课程 + if (errors.isEmpty()) { + List sectionIds = new ArrayList<>(sectionContentMap.keySet()); + long validSections = csectionDao.count( + FieldFilters.eq("courseId", uploadDto.getCourseId()), + FieldFilters.in("id", sectionIds) + ); + + if (validSections != sectionIds.size()) { + errors.add("部分章节ID无效或不属于当前课程"); + } + } + + return errors; + } + + /** + * 获取课程分类树(多级联查) + */ + @GetMapping("/type/tree") + public JsonResponse> getCourseTypeTree() { + try { + List typeTree = typeTreeService.getCourseTypeTree(); + return success(typeTree, "获取课程分类树成功"); + } catch (Exception e) { + log.error("获取课程分类树失败", e); + return error("获取课程分类树失败:" + e.getMessage()); + } + } + + /** + * 获取资源归属树(多级联查) + */ + @GetMapping("/owner/tree") + public JsonResponse> getResourceOwnerTree() { + try { + List orgTree = typeTreeService.getResourceOwnerTree(); + return success(orgTree, "获取资源归属树成功"); + } catch (Exception e) { + log.error("获取资源归属树失败", e); + return error("获取资源归属树失败:" + e.getMessage()); + } + } + + /** + * 根据父级ID获取子分类 + */ + @GetMapping("/type/children") + public JsonResponse> getChildTypes(@RequestParam String parentId) { + try { + if (StringUtils.isBlank(parentId)) { + return badRequest("父级ID不能为空"); + } + List children = typeTreeService.getChildTypes(parentId); + return success(children, "获取子分类成功"); + } catch (Exception e) { + log.error("获取子分类失败", e); + return error("获取子分类失败:" + e.getMessage()); + } + } + + /** + * 根据父级ID获取子组织 + */ + @GetMapping("/owner/children") + public JsonResponse> getChildOrgs(@RequestParam String parentId) { + try { + if (StringUtils.isBlank(parentId)) { + return badRequest("父级ID不能为空"); + } + List children = typeTreeService.getChildOrgs(parentId); + return success(children, "获取子组织成功"); + } catch (Exception e) { + log.error("获取子组织失败", e); + return error("获取子组织失败:" + e.getMessage()); + } + } + + /** + * 根据课程名称查询课程 + */ + @GetMapping("/by-name") + public JsonResponse getCourseByName(@RequestParam String name) { + try { + if (StringUtils.isBlank(name)) { + return badRequest("课程名称不能为空"); + } + + Course course = courseDao.findOne( + FieldFilters.eq("name", name), + FieldFilters.eq("deleted", false) + ); + + if (course == null) { + return success(null, "课程不存在"); + } + + return success(course, "查询成功"); + } catch (Exception e) { + log.error("根据课程名称查询失败", e); + return error("查询失败:" + e.getMessage()); + } + } + // 测试接口 + @PostMapping("/test") + public JsonResponse> test() { + try { + // 1. 查询系统中所有未被删除的课程内容(核心全量数据) + List allContentList = ccDao.findList( + OrderCondition.asc("courseId"), + // 按课程ID+排序号排序 + FieldFilters.eq("deleted", false) // 过滤已删除数据 + ); + + // 2. 无数据时返回空列表+提示 + if (allContentList.isEmpty()) { + return success(Collections.emptyList(), "系统中暂无课程内容数据"); + } + + // 3. 组装每个课程内容的完整DTO(关联作业/考试/评估) + List allDtoList = new ArrayList<>(); + for (CourseContent content : allContentList) { + CourseContentDto dto = new CourseContentDto(); + dto.setContent(content); // 课程内容基础数据 + dto.setHomework(ccontentService.getHomework(content.getId())); // 关联作业 + dto.setExam(ccontentService.getExam(content.getId())); // 关联考试 + dto.setAssess(ccontentService.getAssess(content.getId())); // 关联评估 + allDtoList.add(dto); + } + + log.info("全查询返回数据条数:{}", allDtoList.size()); + + // 5. 返回全量数据 + return success(allDtoList, "全查询成功,共返回 " + allDtoList.size() + " 条课程内容数据"); + + } catch (Exception e) { + log.error("全查询课程内容数据失败", e); + return error("全查询失败:" + e.getMessage()); + } + } @PostMapping("/save-section") public JsonResponse saveSection(CourseSection section){ diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/dao/CourseDao.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/dao/CourseDao.java index bf68498b..2bee3cc4 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/course/dao/CourseDao.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/dao/CourseDao.java @@ -31,6 +31,47 @@ import javax.persistence.Query; public class CourseDao extends BaseDao { @PersistenceContext private EntityManager entityManager; + + /** + * 单条课程查询 + * @param filters 查询条件 + * @return Course对象 + */ + public Course findOneWithoutDuration(List filters) throws Exception { + QueryBuilder query = QueryBuilder.from(Course.class); + // 只查询必要字段,彻底排除courseDuration + query.addFields("id", "name", "deleted", "status", "published", "enabled", "orgId", "sysCreateBy"); + if (filters != null && !filters.isEmpty()) { + query.addFilters(filters); + } + // 只查1条 + query.setPageSize(1); + List list = this.findListFields(query.builder()); + if (list.isEmpty()) { + return null; + } + // 封装为Course对象(仅赋值查询的字段) + Object[] objs = list.get(0); + Course course = new Course(); + course.setId((String) objs[0]); + course.setName((String) objs[1]); + course.setDeleted((Boolean) objs[2]); + course.setStatus((Integer) objs[3]); + course.setPublished((Boolean) objs[4]); + course.setEnabled((Boolean) objs[5]); + course.setOrgId((String) objs[6]); + course.setSysCreateBy((String) objs[7]); + return course; + } + + // 按课程名称查询 + public Course findByNameWithoutDuration(String courseName) throws Exception { + List filters = new ArrayList<>(); + filters.add(FieldFilters.eq("name", courseName)); + filters.add(FieldFilters.eq("deleted", false)); + return findOneWithoutDuration(filters); + } + /** * 课程分页 搜索查询 * */ diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/BatchUploadResponseDto.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/BatchUploadResponseDto.java new file mode 100644 index 00000000..24e4e879 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/BatchUploadResponseDto.java @@ -0,0 +1,41 @@ +package com.xboe.module.course.dto; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 批量上传响应 DTO + */ +@Data +public class BatchUploadResponseDto { + private boolean success; + private String message; + private int totalCount; + private int successCount; + private int failCount; + private List results; + private String courseId; + + @Data + public static class UploadResult { + private int itemIndex; // 标识的是第几个上传项 + private String contentName; + private Integer contentType; + private Integer resourceType; + private boolean success; + private String message; + private String contentId; + private List resources; + } + + @Data + public static class ResourceResult { + private String resourceName; + private boolean success; + private String message; + private String fileId; + private String fileUrl; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/BatchUploadWithNullDto.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/BatchUploadWithNullDto.java new file mode 100644 index 00000000..19250515 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/BatchUploadWithNullDto.java @@ -0,0 +1,256 @@ +package com.xboe.module.course.dto; + +import lombok.Data; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * 批量上传DTO(支持9种资源:视频、音频、文档、图文、SCORM包、外部链接、作业、考试、评估) + */ +@Data +public class BatchUploadWithNullDto { + + @NotBlank(message = "课程ID不能为空") + private String courseId; + + @NotNull(message = "内容项列表不能为空") + private List contentItems; + + /** + * 资源归属信息(课件资源使用) + */ + private ResourceOwnerInfo ownerInfo; + + @NotNull(message = "操作类型不能为空") + private Integer operationType; // 1新增 + + /** + * 内容项DTO(支持9种资源) + */ + @Data + public static class ContentItem { + /** + * 内容ID + */ + private String contentId; + + + @Size(max = 200, message = "内容名称不能超过200字") + private String contentName; + + /** + * 内容类型: + * 1: 课件(包含6种资源类型) + * 2: 作业 + * 3: 考试 + * 4: 评估 + */ + private Integer contentType; + + /** + * 排序索引 + */ + private Integer sortIndex; + + /** + * 课时(分钟) + */ + private Integer duration; + + /** + * 是否删除 + */ + private Boolean deleted; + + /** + * 章节ID(关联到特定的小节) + */ + private String csectionId; + + // ---------- 课件相关字段 ---------- + /** + * 资源类型(仅课件使用): + * 10: 视频 + * 20: 音频 + * 40: 文档 + * 41: 图文 + * 50: SCORM包 + * 90: 外部链接 + */ + private Integer resourceType; + + /** + * 文件资源信息 + */ + private FileResourceInfo fileResource; + + /** + * 图文资源信息 + */ + private GraphicTextResourceInfo graphicTextResource; + + /** + * 外部链接资源信息 + */ + private ExternalLinkResourceInfo externalLinkResource; + + /** + * 作业相关字段 + */ + private HomeworkInfo homeworkInfo; + + /** + * 考试相关字段 + */ + private ExamInfo examInfo; + + /** + * 评估相关字段 + */ + private AssessInfo assessInfo; + + } + + /** + * 文件资源信息 + */ + @Data + public static class FileResourceInfo { + private String fileBase64; + private String originalFileName; + private Boolean down; + private String remark; + private Integer device; + private String decoder; + private Integer videoWidth; + private Integer videoHeight; + } + + /** + * 图文资源信息 + */ + @Data + public static class GraphicTextResourceInfo { + private String title; + private String content; + private String imageBase64; + private String imageName; + private TextFormat format; + private String fontSize; + private String lineHeight; + private String textAlign; + } + + /** + * 外部链接资源信息 + */ + @Data + public static class ExternalLinkResourceInfo { + private String url; + private Integer openType; + private String title; + private String description; + } + + /** + * 作业信息 + */ + @Data + public static class HomeworkInfo { + private String name; + private String content; + private String file; + private LocalDateTime deadTime; + private Integer submitMode; + } + + /** + * 考试信息 + */ + @Data + public static class ExamInfo { + private String testName; + private Integer testDuration; + private Boolean showAnalysis; + private Boolean showAnswer; + private Integer times; + private Integer arrange; + private Integer scoringType; + private Integer passLine; + private Boolean randomMode; + private Integer qnum; + private Float qscore; + private Integer paperType; + private Boolean percentScore; + private String paperId; + private String paperContent; + private String info; + } + + /** + * 评估信息 + */ + @Data + public static class AssessInfo { + private String assessId; + private String question; + private Integer qType; + } + + /** + * 资源归属信息 + */ + @Data + public static class ResourceOwnerInfo { + private String orgId; + private String orgName; + private String resOwner1; + private String resOwner2; + private String resOwner3; + private String ownership1; + private String ownership2; + private String ownership3; + } + + /** + * 文本格式 + */ + @Data + public static class TextFormat { + private Boolean bold; + private Boolean italic; + private Boolean underline; + private Boolean strikethrough; + } + + /** + * 验证整个DTO + */ + public List validate() { + List errors = new ArrayList<>(); + + if (StringUtils.isBlank(courseId)) { + errors.add("课程ID不能为空"); + } + + if (operationType == null) { + errors.add("操作类型不能为空"); + } else if (operationType != 1 ) { + errors.add("操作类型必须是1(新增)"); + } + + if (CollectionUtils.isEmpty(contentItems)) { + errors.add("内容项列表不能为空"); + } + + return errors; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/CourseResourceUploadDto.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/CourseResourceUploadDto.java new file mode 100644 index 00000000..edb62b37 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/CourseResourceUploadDto.java @@ -0,0 +1,134 @@ +package com.xboe.module.course.dto; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; + +/** + * 课程资源上传 DTO(用于单个课件资源处理) + */ +@Data +public class CourseResourceUploadDto { + + @NotBlank(message = "课程ID不能为空") + private String courseId; + + @NotNull(message = "资源列表不能为空") + private List resources; + + /** + * 资源归属信息 + */ + private ResourceOwnerInfo ownerInfo; + + /** + * 资源归属信息DTO + */ + @Data + public static class ResourceOwnerInfo { + private String orgId; + private String orgName; + private String resOwner1; + private String resOwner2; + private String resOwner3; + private String ownership1; + private String ownership2; + private String ownership3; + } + + /** + * 课程文件资源 + */ + @Data + public static class CourseFileResource { + /** + * 资源类型: + * 10: 视频 + * 20: 音频 + * 40: 文档 + * 41: 图文 + * 50: SCORM包 + * 90: 外部链接 + */ + @NotNull(message = "资源类型不能为空") + private Integer resType; + + @NotBlank(message = "资源名称不能为空") + @Size(max = 100, message = "资源名称不能超过100字") + private String name; + + private String fileName; + private String fileType; + private Integer fileSize; + private String filePath; + private String previewFilePath; + private Integer duration; + private Boolean down = false; + + @Size(max = 200, message = "备注不能超过200字") + private String remark; + + private Integer device; + private String fileBase64; + private String originalFileName; + private GraphicTextContent graphicText; + private ExternalLinkContent externalLink; + private String decoder; + private Integer videoWidth; + private Integer videoHeight; + private String ownership1; + private String ownership2; + private String ownership3; + private Integer converStatus = 0; + private String converError; + } + + /** + * 图文内容DTO + */ + @Data + public static class GraphicTextContent { + @NotBlank(message = "图文标题不能为空") + @Size(max = 100, message = "图文标题不能超过100字") + private String title; + + @NotBlank(message = "图文内容不能为空") + private String content; + + private String imageBase64; + private String imageName; + private TextFormat format; + private String fontSize; + private String lineHeight; + private String textAlign; + } + + /** + * 外部链接内容DTO + */ + @Data + public static class ExternalLinkContent { + @NotBlank(message = "链接地址不能为空") + private String url; + + private Integer openType = 2; + private String title; + private String description; + } + + /** + * 文本格式DTO + */ + @Data + public static class TextFormat { + private Boolean bold = false; + private Boolean italic = false; + private Boolean underline = false; + private Boolean strikethrough = false; + } + +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/CourseWareSaveDto.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/CourseWareSaveDto.java new file mode 100644 index 00000000..c3cc1a89 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/CourseWareSaveDto.java @@ -0,0 +1,113 @@ +package com.xboe.module.course.dto; + +import com.xboe.module.course.entity.CourseCrowd; +import com.xboe.module.course.entity.CourseTeacher; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; + +/** + * 课程课件保存/编辑 DTO + */ +@Data +public class CourseWareSaveDto { + + @NotNull(message = "课程基础信息不能为空") + private CourseInfo course; + + + @NotNull(message = "至少选择一位授课教师") + private List teacherList; + + + private List crowdList; + + /** + * 课程分类(三级) + */ + @Data + public static class TypeLevelInfo { + @NotBlank(message = "分类ID不能为空") + @Size(max = 20, message = "分类ID长度不能超过20位") + private String id; + + @Size(max = 20, message = "分类名称长度不能超过20位") + private String name; + + private String parentId; + } + + @NotNull(message = "课程一级分类不能为空") + private TypeLevelInfo sysType1; + + private TypeLevelInfo sysType2; + + private TypeLevelInfo sysType3; + + /** + * 资源归属(三级) + */ + @Data + public static class OrgLevelInfo { + @NotBlank(message = "组织ID不能为空") + @Size(max = 20, message = "组织ID长度不能超过20位") + private String id; + + @Size(max = 50, message = "组织名称长度不能超过50位") + private String name; + + private String parentId; + } + + @NotNull(message = "资源一级归属不能为空") + private OrgLevelInfo resOwner1; + + private OrgLevelInfo resOwner2; + + private OrgLevelInfo resOwner3; + + @NotNull(message = "观看设置不能为空") + private Integer device; + + /** + * 课程信息DTO + */ + @Data + public static class CourseInfo { + private String id; + private Integer sysVersion; + + @NotBlank(message = "课程名称不能为空") + @Size(max = 100, message = "课程名称不能超过100字") + private String name; + + @Size(max = 200, message = "课程价值不能超过200字") + private String value; + + @Size(max = 500, message = "课程简介不能超过500字") + private String summary; + + @Size(max = 50, message = "目标人群不能超过50字") + private String forUsers; + + @Size(max = 200, message = "课程标签不能超过200字") + private String tags; + + private String coverImg; + private Integer type; + private String orgId; + private Boolean enabled = true; + private Boolean isTop = false; + private Integer studyTime; + private String overview; + private Integer openCourse; + private Boolean visible = true; + private Boolean orderStudy = false; + private String keywords; + private String forScene; + private String createFrom; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/HierarchicalCourseDto.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/HierarchicalCourseDto.java new file mode 100644 index 00000000..0954bf76 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/dto/HierarchicalCourseDto.java @@ -0,0 +1,53 @@ +package com.xboe.module.course.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 层级结构课程DTO + */ +@Data +public class HierarchicalCourseDto { + + @NotBlank(message = "课程ID不能为空") + private String courseId; + + @NotNull(message = "操作类型不能为空") + private Integer operationType; + + private BatchUploadWithNullDto.ResourceOwnerInfo ownerInfo; + + @NotEmpty(message = "章列表不能为空") + private List chapters; + + @Data + public static class Chapter { + + @NotBlank(message = "章ID不能为空") + private String chapterId; + + private String chapterName; + + @NotEmpty(message = "节列表不能为空") + private List
sections; + } + + /** + * 节信息 + */ + @Data + public static class Section { + + @NotBlank(message = "节ID不能为空") + private String sectionId; + + private String sectionName; + + @NotNull(message = "内容项不能为空") + private BatchUploadWithNullDto.ContentItem content; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/entity/Course.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/entity/Course.java index ba84981c..137c8431 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/course/entity/Course.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/entity/Course.java @@ -126,7 +126,7 @@ public class Course extends BaseEntity { /** * 课程名称 * */ - @Column(name = "name",nullable=false, length = 100) + @Column(name = "name",nullable=false, length = 50) private String name; @Column(name = "fulltext_id",length = 36) @@ -409,13 +409,15 @@ public class Course extends BaseEntity { /** * 课程时长(秒) */ - @Column(name = "course_duration") +// @Column(name = "course_duration") + @Transient private Long courseDuration; /** * 排序权重 */ - @Column(name = "sort_weight") +// @Column(name = "sort_weight") + @Transient private Integer sortWeight; /** @@ -423,7 +425,8 @@ public class Course extends BaseEntity { * teacher-教师端 * admin-管理员端 */ - @Column(name = "create_from") +// @Column(name = "create_from") + @Transient private String createFrom; public Course(String id,String name,String summary,String coverImg,String sysCreateAid,String sysCreateBy,Integer type,LocalDateTime publishTime){ diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/CourseContentValidationService.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/CourseContentValidationService.java new file mode 100644 index 00000000..c1de07bc --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/CourseContentValidationService.java @@ -0,0 +1,63 @@ +package com.xboe.module.course.service; + +import com.xboe.module.course.dto.BatchUploadResponseDto; +import com.xboe.module.course.dto.BatchUploadWithNullDto; +import com.xboe.module.course.dto.CourseResourceUploadDto; + +import java.util.List; + +/** + * 课程内容验证 服务接口 + */ +public interface CourseContentValidationService { + + /** + * 验证批量上传DTO + */ + List validateBatchUpload(BatchUploadWithNullDto dto); + + /** + * 验证内容项 + */ + List validateContentItem(BatchUploadWithNullDto.ContentItem item, Integer operationType, int index); + + /** + * 验证资源上传DTO + */ + String validateResource(CourseResourceUploadDto.CourseFileResource resource); + + /** + * 验证资源类型是否有效 + */ + boolean isValidResourceType(Integer resType); + + /** + * 验证文件扩展名 + */ + boolean isValidFileExtension(Integer resType, String fileName); + + /** + * 获取文件扩展名 + */ + String getFileExtension(String filename); + + /** + * 获取资源类型名称 + */ + String getResourceTypeName(Integer resType); + + /** + * 创建一个新的批量上传响应DTO + * @param courseId 课程ID + * @param totalCount 总数 + * @return BatchUploadResponseDto + */ + BatchUploadResponseDto createBatchUploadResponse(String courseId, int totalCount); + + /** + * 添加结果到批量上传响应DTO + * @param response 响应DTO + * @param result 结果 + */ + void addResultToBatchUploadResponse(BatchUploadResponseDto response, BatchUploadResponseDto.UploadResult result); +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/CourseResourceService.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/CourseResourceService.java new file mode 100644 index 00000000..383598a6 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/CourseResourceService.java @@ -0,0 +1,54 @@ +package com.xboe.module.course.service; + +import com.xboe.module.course.dto.CourseResourceUploadDto; +import com.xboe.module.course.entity.CourseFile; + +import java.util.List; + +/** + * 课程资源服务接口 + */ +public interface CourseResourceService { + + /** + * 处理课程资源(核心方法) + * + * @param courseId 课程ID + * @param resources 资源列表 + * @param orgId 组织ID + * @param orgName 组织名称 + * @param resOwner1 资源归属1级 + * @param resOwner2 资源归属2级 + * @param resOwner3 资源归属3级 + * @param ownership1 所有权1级 + * @param ownership2 所有权2级 + * @param ownership3 所有权3级 + */ + void processCourseResources(String courseId, + List resources, + String orgId, String orgName, + String resOwner1, String resOwner2, String resOwner3, + String ownership1, String ownership2, String ownership3); + + /** + * 获取课程资源列表 + * + * @param courseId 课程ID + * @return 资源列表 + */ + List getCourseResources(String courseId); + + /** + * 删除课程资源 + * + * @param courseId 课程ID + */ + void deleteCourseResources(String courseId); + + /** + * 删除单个资源 + * + * @param resourceId 资源ID + */ + void deleteResource(String resourceId); +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/IAtomicBatchUploadService.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/IAtomicBatchUploadService.java new file mode 100644 index 00000000..3b98d17a --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/IAtomicBatchUploadService.java @@ -0,0 +1,18 @@ +package com.xboe.module.course.service; + +import com.xboe.module.course.dto.BatchUploadWithNullDto; +import com.xboe.module.course.dto.BatchUploadResponseDto; + +/** + * 原子批量上传服务接口 + * 支持9种资源的上传 + */ +public interface IAtomicBatchUploadService { + + /** + * 原子批量上传 + * @param uploadDto 上传数据 + * @return 上传结果 + */ + BatchUploadResponseDto atomicBatchUpload(BatchUploadWithNullDto uploadDto); +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/ICourseContentService.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/ICourseContentService.java index 46cfa4f1..7b120a21 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/ICourseContentService.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/ICourseContentService.java @@ -2,16 +2,18 @@ package com.xboe.module.course.service; import com.xboe.common.PageList; import com.xboe.module.course.dto.CourseContentDto; +import com.xboe.module.course.dto.CourseWareSaveDto; import com.xboe.module.course.dto.SortItem; import com.xboe.module.course.entity.CourseAssess; import com.xboe.module.course.entity.CourseContent; import com.xboe.module.course.entity.CourseExam; import com.xboe.module.course.entity.CourseHomeWork; + import java.util.List; /** - * 课程内容,当前是分着处理,之后看是否与课程服务合并在一起 + * 课程内容 * */ public interface ICourseContentService{ @@ -20,6 +22,13 @@ public interface ICourseContentService{ * @param dto */ void saveOrUpdate(CourseContentDto dto); + + /** + * 保存更新课件 + * @param dto + * @return + */ + void saveOrUpdateCourseware(CourseWareSaveDto dto); /** @@ -37,13 +46,12 @@ public interface ICourseContentService{ void updateName(String id,String name); /** - * 对于已发布过的课程 ,采用逻辑删除,对于未发布过的课程采用物理删除 * @param id */ void delete(String id,int ctype, boolean flag); /** - * 用于检查课程是否完整,是否可以提交了,只是部分必要的字段 + * 用于检查课程是否完整,是否可以提交了 * @param courseId * @return */ @@ -58,7 +66,6 @@ public interface ICourseContentService{ /** * 根据课程id、章节id得到课程所有目录(即章节,分页),顺序按orderIndex 从小到大的顺序 - * 25.11.26新增 * * @param pageIndex 页码 * @param pageSize 每页数量 diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/TypeTreeService.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/TypeTreeService.java new file mode 100644 index 00000000..31a9d941 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/TypeTreeService.java @@ -0,0 +1,37 @@ +package com.xboe.module.course.service; + +import com.xboe.module.course.vo.TypeTreeVo; +import com.xboe.system.organization.vo.OrganizationVo; + +import java.util.List; + +/** + * 分类树和组织树服务接口 + */ +public interface TypeTreeService { + + /** + * 获取课程分类树(三级) + */ + List getCourseTypeTree(); + + /** + * 获取资源归属树(组织机构树) + */ + List getResourceOwnerTree(); + + /** + * 根据父级ID获取子分类 + */ + List getChildTypes(String parentId); + + /** + * 根据父级ID获取子组织 + */ + List getChildOrgs(String parentId); + + /** + * 验证分类层级关系 + */ + boolean validateTypeHierarchy(String childId, String parentId); +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/AtomicBatchUploadServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/AtomicBatchUploadServiceImpl.java new file mode 100644 index 00000000..7b076075 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/AtomicBatchUploadServiceImpl.java @@ -0,0 +1,707 @@ +package com.xboe.module.course.service.impl; + +import com.xboe.core.orm.FieldFilters; +import com.xboe.module.course.dao.*; +import com.xboe.module.course.dto.BatchUploadWithNullDto; +import com.xboe.module.course.dto.BatchUploadResponseDto; +import com.xboe.module.course.dto.CourseResourceUploadDto; +import com.xboe.module.course.entity.*; +import com.xboe.module.course.service.CourseContentValidationService; +import com.xboe.module.course.service.IAtomicBatchUploadService; +import com.xboe.module.course.service.CourseResourceService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Service +public class AtomicBatchUploadServiceImpl implements IAtomicBatchUploadService { + + @Resource + private CourseContentDao courseContentDao; + + @Resource + private CourseFileDao courseFileDao; + + @Resource + private CourseHomeWorkDao courseHomeWorkDao; + + @Resource + private CourseExamDao courseExamDao; + + @Resource + private CourseAssessDao courseAssessDao; + + @Resource + private CourseResourceService courseResourceService; + + @Resource + private CourseContentValidationService courseContentValidationService; + + /** + * 原子批量上传 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public BatchUploadResponseDto atomicBatchUpload(BatchUploadWithNullDto uploadDto) { + log.info("开始原子批量上传,课程ID: {}, 操作类型: {}, 内容项数量: {}", + uploadDto.getCourseId(), + uploadDto.getOperationType(), + uploadDto.getContentItems().size()); + + // 创建响应对象 + BatchUploadResponseDto response = courseContentValidationService.createBatchUploadResponse( + uploadDto.getCourseId(), + uploadDto.getContentItems().size() + ); + + try { + // 验证输入 + List validationErrors = uploadDto.validate(); + if (CollectionUtils.isNotEmpty(validationErrors)) { + throw new RuntimeException("参数验证失败: " + String.join("; ", validationErrors)); + } + + // 设置资源归属信息(用于课件资源) + BatchUploadWithNullDto.ResourceOwnerInfo ownerInfo = uploadDto.getOwnerInfo(); + String orgId = ownerInfo != null ? ownerInfo.getOrgId() : null; + String orgName = ownerInfo != null ? ownerInfo.getOrgName() : null; + String resOwner1 = ownerInfo != null ? ownerInfo.getResOwner1() : null; + String resOwner2 = ownerInfo != null ? ownerInfo.getResOwner2() : null; + String resOwner3 = ownerInfo != null ? ownerInfo.getResOwner3() : null; + String ownership1 = ownerInfo != null ? ownerInfo.getOwnership1() : null; + String ownership2 = ownerInfo != null ? ownerInfo.getOwnership2() : null; + String ownership3 = ownerInfo != null ? ownerInfo.getOwnership3() : null; + + // 处理每个内容项 + for (int i = 0; i < uploadDto.getContentItems().size(); i++) { + BatchUploadWithNullDto.ContentItem item = uploadDto.getContentItems().get(i); + BatchUploadResponseDto.UploadResult itemResult = processContentItem( + item, i, uploadDto.getOperationType(), uploadDto.getCourseId(), + item.getCsectionId(), orgId, orgName, resOwner1, resOwner2, + resOwner3, ownership1, ownership2, ownership3 + ); + + courseContentValidationService.addResultToBatchUploadResponse(response, itemResult); + + // 如果有失败项,抛出异常触发回滚 + if (!itemResult.isSuccess()) { + throw new RuntimeException("第" + (i + 1) + "项处理失败: " + itemResult.getMessage()); + } + } + + log.info("原子批量上传成功,课程ID: {}, 成功: {}", + uploadDto.getCourseId(), response.getSuccessCount()); + + return response; + + } catch (Exception e) { + log.error("原子批量上传失败,课程ID: {}", uploadDto.getCourseId(), e); + throw new RuntimeException("原子批量上传失败: " + e.getMessage(), e); + } + } + + /** + * 处理单个内容项 + */ + private BatchUploadResponseDto.UploadResult processContentItem( + BatchUploadWithNullDto.ContentItem item, int itemIndex, Integer operationType, + String courseId, String csectionId, + String orgId, String orgName, String resOwner1, String resOwner2, String resOwner3, + String ownership1, String ownership2, String ownership3) { + + BatchUploadResponseDto.UploadResult result = new BatchUploadResponseDto.UploadResult(); + result.setItemIndex(itemIndex); + result.setContentName(item.getContentName()); + result.setContentType(item.getContentType()); + result.setResourceType(item.getResourceType()); + + try { + String contentId; + + if (operationType == 1) { // 新增操作 + contentId = createContentItem(item, courseId, csectionId, + orgId, orgName, resOwner1, resOwner2, resOwner3, + ownership1, ownership2, ownership3); + } else if (operationType == 2) { // 更新操作 + contentId = updateContentItem(item, courseId, csectionId, + orgId, orgName, resOwner1, resOwner2, resOwner3, + ownership1, ownership2, ownership3); + } else { + throw new RuntimeException("不支持的操作类型: " + operationType); + } + + result.setContentId(contentId); + result.setSuccess(true); + result.setMessage("处理成功"); + + } catch (Exception e) { + result.setSuccess(false); + result.setMessage("处理失败: " + e.getMessage()); + log.error("处理第{}项内容失败: {}", itemIndex + 1, e.getMessage(), e); + } + + return result; + } + + /** + * 创建内容项 + */ + private String createContentItem( + BatchUploadWithNullDto.ContentItem item, String courseId, String csectionId, + String orgId, String orgName, String resOwner1, String resOwner2, String resOwner3, + String ownership1, String ownership2, String ownership3) { + + // 验证新增操作的必要字段 + if (item.getContentType() == null) { + throw new RuntimeException("新增操作时内容类型不能为null"); + } + + // 创建课程内容 + CourseContent courseContent = new CourseContent(); + courseContent.setCourseId(courseId); + courseContent.setContentName(getContentName(item, true)); + courseContent.setContentType(item.getContentType()); + courseContent.setSortIndex(item.getSortIndex() != null ? item.getSortIndex() : 0); + courseContent.setCsectionId(csectionId); + courseContent.setDuration(item.getDuration() != null ? item.getDuration() : 0); + courseContent.setDeleted(item.getDeleted() != null ? item.getDeleted() : false); + + courseContentDao.save(courseContent); + String contentId = courseContent.getId(); + log.info("创建课程内容成功,ID: {}, 类型: {}", contentId, item.getContentType()); + + // 根据内容类型处理具体内容 + processContentByType(item, contentId, courseId, 1, + orgId, orgName, resOwner1, resOwner2, resOwner3, + ownership1, ownership2, ownership3); + + return contentId; + } + + /** + * 更新内容项(更新操作) + */ + private String updateContentItem( + BatchUploadWithNullDto.ContentItem item, String courseId, String csectionId, + String orgId, String orgName, String resOwner1, String resOwner2, String resOwner3, + String ownership1, String ownership2, String ownership3) { + + // 验证内容ID + if (StringUtils.isBlank(item.getContentId())) { + throw new RuntimeException("更新操作时内容ID不能为空"); + } + + // 获取现有内容 + CourseContent courseContent = courseContentDao.get(item.getContentId()); + if (courseContent == null) { + throw new RuntimeException("课程内容不存在: " + item.getContentId()); + } + + // 验证课程ID匹配 + if (!courseContent.getCourseId().equals(courseId)) { + throw new RuntimeException("课程ID不匹配"); + } + + String contentId = item.getContentId(); + + // 更新课程内容(只更新非null字段) + updateCourseContentWithNull(courseContent, item, csectionId); + courseContentDao.update(courseContent); + log.info("更新课程内容成功,ID: {}", contentId); + + // 根据内容类型处理具体内容 + processContentByType(item, contentId, courseId, 2, + orgId, orgName, resOwner1, resOwner2, resOwner3, + ownership1, ownership2, ownership3); + + return contentId; + } + + /** + * 更新课程内容(只更新非null字段) + */ + private void updateCourseContentWithNull(CourseContent courseContent, + BatchUploadWithNullDto.ContentItem item, + String csectionId) { + if (item.getContentName() != null) { + courseContent.setContentName(item.getContentName()); + } + if (item.getContentType() != null) { + courseContent.setContentType(item.getContentType()); + } + if (item.getSortIndex() != null) { + courseContent.setSortIndex(item.getSortIndex()); + } + if (csectionId != null) { + courseContent.setCsectionId(csectionId); + } + if (item.getDuration() != null) { + courseContent.setDuration(item.getDuration()); + } + if (item.getDeleted() != null) { + courseContent.setDeleted(item.getDeleted()); + } + } + + /** + * 根据内容类型处理具体内容 + */ + private void processContentByType( + BatchUploadWithNullDto.ContentItem item, String contentId, String courseId, + Integer operationType, + String orgId, String orgName, String resOwner1, String resOwner2, String resOwner3, + String ownership1, String ownership2, String ownership3) { + + Integer contentType = item.getContentType(); + + switch (contentType != null ? contentType : 0) { + case 1: // 课件 + processCourseware(item, contentId, courseId, operationType, + orgId, orgName, resOwner1, resOwner2, resOwner3, + ownership1, ownership2, ownership3); + break; + case 2: // 作业 + processHomework(item, contentId, courseId, operationType); + break; + case 3: // 考试 + processExam(item, contentId, courseId, operationType); + break; + case 4: // 评估 + processAssess(item, contentId, courseId, operationType); + break; + case 0: // contentType为null(更新时不修改类型) + // 不处理,保持原内容类型 + break; + } + } + + /** + * 处理课件 + */ + private void processCourseware( + BatchUploadWithNullDto.ContentItem item, String contentId, String courseId, + Integer operationType, + String orgId, String orgName, String resOwner1, String resOwner2, String resOwner3, + String ownership1, String ownership2, String ownership3) { + + // 如果是新增操作,resourceType不能为null + if (operationType == 1 && item.getResourceType() == null) { + throw new RuntimeException("新增课件时资源类型不能为null"); + } + + // 如果resourceType不为null,处理对应的资源 + if (item.getResourceType() != null) { + List resources = + convertToCourseFileResources(item); + + if (!resources.isEmpty()) { + // 调用资源处理服务 + courseResourceService.processCourseResources(contentId, resources, + orgId, orgName, + resOwner1, resOwner2, resOwner3, + ownership1, ownership2, ownership3); + log.info("处理课件资源成功,内容ID: {}, 资源类型: {}", contentId, item.getResourceType()); + } + } + } + + /** + * 转换为课程文件资源 + */ + private List convertToCourseFileResources( + BatchUploadWithNullDto.ContentItem item) { + + List resources = new ArrayList<>(); + + // 根据资源类型创建对应的资源 + if (item.getResourceType() != null) { + CourseResourceUploadDto.CourseFileResource resource = + new CourseResourceUploadDto.CourseFileResource(); + + resource.setResType(item.getResourceType()); + resource.setName(item.getContentName() != null ? item.getContentName() : + getResourceTypeName(item.getResourceType()) + "资源"); + resource.setDevice(3); // 默认多端可见 + + // 处理文件资源(视频、音频、文档、SCORM包) + if (item.getFileResource() != null && + (item.getResourceType() == 10 || item.getResourceType() == 20 || + item.getResourceType() == 40 || item.getResourceType() == 50)) { + + BatchUploadWithNullDto.FileResourceInfo fileInfo = item.getFileResource(); + resource.setFileBase64(fileInfo.getFileBase64()); + resource.setOriginalFileName(fileInfo.getOriginalFileName()); + resource.setDown(fileInfo.getDown()); + resource.setRemark(fileInfo.getRemark()); + resource.setDevice(fileInfo.getDevice() != null ? fileInfo.getDevice() : 3); + resource.setDecoder(fileInfo.getDecoder()); + resource.setVideoWidth(fileInfo.getVideoWidth()); + resource.setVideoHeight(fileInfo.getVideoHeight()); + } + + // 处理图文资源 + if (item.getGraphicTextResource() != null && item.getResourceType() == 41) { + BatchUploadWithNullDto.GraphicTextResourceInfo textInfo = item.getGraphicTextResource(); + CourseResourceUploadDto.GraphicTextContent graphicText = + new CourseResourceUploadDto.GraphicTextContent(); + + graphicText.setTitle(textInfo.getTitle()); + graphicText.setContent(textInfo.getContent()); + graphicText.setImageBase64(textInfo.getImageBase64()); + graphicText.setImageName(textInfo.getImageName()); + + if (textInfo.getFormat() != null) { + CourseResourceUploadDto.TextFormat format = new CourseResourceUploadDto.TextFormat(); + format.setBold(textInfo.getFormat().getBold()); + format.setItalic(textInfo.getFormat().getItalic()); + format.setUnderline(textInfo.getFormat().getUnderline()); + format.setStrikethrough(textInfo.getFormat().getStrikethrough()); + graphicText.setFormat(format); + } + + graphicText.setFontSize(textInfo.getFontSize()); + graphicText.setLineHeight(textInfo.getLineHeight()); + graphicText.setTextAlign(textInfo.getTextAlign()); + + resource.setGraphicText(graphicText); + } + + // 处理外部链接资源 + if (item.getExternalLinkResource() != null && item.getResourceType() == 90) { + BatchUploadWithNullDto.ExternalLinkResourceInfo linkInfo = item.getExternalLinkResource(); + CourseResourceUploadDto.ExternalLinkContent externalLink = + new CourseResourceUploadDto.ExternalLinkContent(); + + externalLink.setUrl(linkInfo.getUrl()); + externalLink.setOpenType(linkInfo.getOpenType() != null ? linkInfo.getOpenType() : 2); + externalLink.setTitle(linkInfo.getTitle()); + externalLink.setDescription(linkInfo.getDescription()); + + resource.setExternalLink(externalLink); + } + + resources.add(resource); + } + + return resources; + } + + /** + * 处理作业 + */ + private void processHomework(BatchUploadWithNullDto.ContentItem item, + String contentId, String courseId, + Integer operationType) { + + if (operationType == 1) { // 新增操作 + if (item.getHomeworkInfo() == null) { + throw new RuntimeException("新增作业时作业信息不能为null"); + } + + BatchUploadWithNullDto.HomeworkInfo homeworkInfo = item.getHomeworkInfo(); + CourseHomeWork homework = new CourseHomeWork(); + homework.setContentId(contentId); + homework.setCourseId(courseId); + homework.setName(homeworkInfo.getName()); + homework.setContent(homeworkInfo.getContent()); + homework.setFile(homeworkInfo.getFile()); + homework.setDeadTime(homeworkInfo.getDeadTime()); + homework.setSubmitMode(homeworkInfo.getSubmitMode() != null ? + homeworkInfo.getSubmitMode() : 1); + + courseHomeWorkDao.save(homework); + log.info("创建作业成功,内容ID: {}", contentId); + + } else if (operationType == 2 && item.getHomeworkInfo() != null) { // 更新操作 + // 更新操作:查找现有作业 + CourseHomeWork existingHomework = courseHomeWorkDao.findOne( + FieldFilters.eq("contentId", contentId)); + + if (existingHomework != null) { + // 更新现有作业(只更新非null字段) + updateHomeworkWithNull(existingHomework, item.getHomeworkInfo()); + courseHomeWorkDao.update(existingHomework); + log.info("更新作业成功,内容ID: {}", contentId); + } else { + // 创建新作业 + BatchUploadWithNullDto.HomeworkInfo homeworkInfo = item.getHomeworkInfo(); + CourseHomeWork homework = new CourseHomeWork(); + homework.setContentId(contentId); + homework.setCourseId(courseId); + homework.setName(homeworkInfo.getName()); + homework.setContent(homeworkInfo.getContent()); + homework.setFile(homeworkInfo.getFile()); + homework.setDeadTime(homeworkInfo.getDeadTime()); + homework.setSubmitMode(homeworkInfo.getSubmitMode() != null ? + homeworkInfo.getSubmitMode() : 1); + + courseHomeWorkDao.save(homework); + log.info("创建作业成功,内容ID: {}", contentId); + } + } + } + + /** + * 更新作业(只更新非null字段) + */ + private void updateHomeworkWithNull(CourseHomeWork homework, + BatchUploadWithNullDto.HomeworkInfo homeworkInfo) { + if (homeworkInfo.getName() != null) { + homework.setName(homeworkInfo.getName()); + } + if (homeworkInfo.getContent() != null) { + homework.setContent(homeworkInfo.getContent()); + } + if (homeworkInfo.getFile() != null) { + homework.setFile(homeworkInfo.getFile()); + } + if (homeworkInfo.getDeadTime() != null) { + homework.setDeadTime(homeworkInfo.getDeadTime()); + } + if (homeworkInfo.getSubmitMode() != null) { + homework.setSubmitMode(homeworkInfo.getSubmitMode()); + } + } + + /** + * 处理考试 + */ + private void processExam(BatchUploadWithNullDto.ContentItem item, + String contentId, String courseId, + Integer operationType) { + + if (operationType == 1) { // 新增操作 + if (item.getExamInfo() == null) { + throw new RuntimeException("新增考试时考试信息不能为null"); + } + + CourseExam exam = createExamFromInfo(contentId, courseId, item.getExamInfo()); + courseExamDao.save(exam); + log.info("创建考试成功,内容ID: {}", contentId); + + } else if (operationType == 2 && item.getExamInfo() != null) { // 更新操作 + // 更新操作:查找现有考试 + CourseExam existingExam = courseExamDao.findOne( + FieldFilters.eq("contentId", contentId)); + + if (existingExam != null) { + // 更新现有考试(只更新非null字段) + updateExamWithNull(existingExam, item.getExamInfo()); + courseExamDao.update(existingExam); + log.info("更新考试成功,内容ID: {}", contentId); + } else { + // 创建新考试 + CourseExam exam = createExamFromInfo(contentId, courseId, item.getExamInfo()); + courseExamDao.save(exam); + log.info("创建考试成功,内容ID: {}", contentId); + } + } + } + + /** + * 创建考试信息 + */ + private CourseExam createExamFromInfo(String contentId, String courseId, + BatchUploadWithNullDto.ExamInfo examInfo) { + CourseExam exam = new CourseExam(); + exam.setContentId(contentId); + exam.setCourseId(courseId); + exam.setTestName(examInfo.getTestName()); + exam.setTestDuration(examInfo.getTestDuration()); + exam.setShowAnalysis(examInfo.getShowAnalysis() != null ? examInfo.getShowAnalysis() : false); + exam.setShowAnswer(examInfo.getShowAnswer() != null ? examInfo.getShowAnswer() : false); + exam.setTimes(examInfo.getTimes() != null ? examInfo.getTimes() : 0); + exam.setArrange(examInfo.getArrange() != null ? examInfo.getArrange() : 0); + exam.setScoringType(examInfo.getScoringType() != null ? examInfo.getScoringType() : 1); + exam.setPassLine(examInfo.getPassLine() != null ? examInfo.getPassLine() : 60); + exam.setRandomMode(examInfo.getRandomMode() != null ? examInfo.getRandomMode() : false); + exam.setQnum(examInfo.getQnum()); + exam.setQscore(examInfo.getQscore()); + exam.setPaperType(examInfo.getPaperType()); + exam.setPercentScore(examInfo.getPercentScore() != null ? examInfo.getPercentScore() : true); + exam.setPaperId(examInfo.getPaperId()); + exam.setPaperContent(examInfo.getPaperContent()); + exam.setInfo(examInfo.getInfo()); + + return exam; + } + + /** + * 更新考试信息 + */ + private void updateExamWithNull(CourseExam exam, BatchUploadWithNullDto.ExamInfo examInfo) { + if (examInfo.getTestName() != null) { + exam.setTestName(examInfo.getTestName()); + } + if (examInfo.getTestDuration() != null) { + exam.setTestDuration(examInfo.getTestDuration()); + } + if (examInfo.getShowAnalysis() != null) { + exam.setShowAnalysis(examInfo.getShowAnalysis()); + } + if (examInfo.getShowAnswer() != null) { + exam.setShowAnswer(examInfo.getShowAnswer()); + } + if (examInfo.getTimes() != null) { + exam.setTimes(examInfo.getTimes()); + } + if (examInfo.getArrange() != null) { + exam.setArrange(examInfo.getArrange()); + } + if (examInfo.getScoringType() != null) { + exam.setScoringType(examInfo.getScoringType()); + } + if (examInfo.getPassLine() != null) { + exam.setPassLine(examInfo.getPassLine()); + } + if (examInfo.getRandomMode() != null) { + exam.setRandomMode(examInfo.getRandomMode()); + } + if (examInfo.getQnum() != null) { + exam.setQnum(examInfo.getQnum()); + } + if (examInfo.getQscore() != null) { + exam.setQscore(examInfo.getQscore()); + } + if (examInfo.getPaperType() != null) { + exam.setPaperType(examInfo.getPaperType()); + } + if (examInfo.getPercentScore() != null) { + exam.setPercentScore(examInfo.getPercentScore()); + } + if (examInfo.getPaperId() != null) { + exam.setPaperId(examInfo.getPaperId()); + } + if (examInfo.getPaperContent() != null) { + exam.setPaperContent(examInfo.getPaperContent()); + } + if (examInfo.getInfo() != null) { + exam.setInfo(examInfo.getInfo()); + } + } + + /** + * 处理评估 + */ + private void processAssess(BatchUploadWithNullDto.ContentItem item, + String contentId, String courseId, + Integer operationType) { + + if (operationType == 1) { // 新增操作 + if (item.getAssessInfo() == null) { + throw new RuntimeException("新增评估时评估信息不能为null"); + } + + CourseAssess assess = createAssessFromInfo(contentId, courseId, item.getAssessInfo()); + courseAssessDao.save(assess); + log.info("创建评估成功,内容ID: {}", contentId); + + } else if (operationType == 2 && item.getAssessInfo() != null) { // 更新操作 + // 更新操作:查找现有评估 + CourseAssess existingAssess = courseAssessDao.findOne( + FieldFilters.eq("contentId", contentId)); + + if (existingAssess != null) { + // 更新现有评估(只更新非null字段) + updateAssessWithNull(existingAssess, item.getAssessInfo()); + courseAssessDao.update(existingAssess); + log.info("更新评估成功,内容ID: {}", contentId); + } else { + // 创建新评估 + CourseAssess assess = createAssessFromInfo(contentId, courseId, item.getAssessInfo()); + courseAssessDao.save(assess); + log.info("创建评估成功,内容ID: {}", contentId); + } + } + } + + /** + * 创建评估信息 + */ + private CourseAssess createAssessFromInfo(String contentId, String courseId, + BatchUploadWithNullDto.AssessInfo assessInfo) { + CourseAssess assess = new CourseAssess(); + assess.setContentId(contentId); + assess.setCourseId(courseId); + assess.setAssessId(assessInfo.getAssessId()); + assess.setQuestion(assessInfo.getQuestion()); + assess.setQType(assessInfo.getQType() != null ? assessInfo.getQType() : 1); + + return assess; + } + + /** + * 更新评估信息 + */ + private void updateAssessWithNull(CourseAssess assess, BatchUploadWithNullDto.AssessInfo assessInfo) { + if (assessInfo.getAssessId() != null) { + assess.setAssessId(assessInfo.getAssessId()); + } + if (assessInfo.getQuestion() != null) { + assess.setQuestion(assessInfo.getQuestion()); + } + if (assessInfo.getQType() != null) { + assess.setQType(assessInfo.getQType()); + } + } + + /** + * 获取内容名称 + */ + private String getContentName(BatchUploadWithNullDto.ContentItem item, boolean isNew) { + if (item.getContentName() != null) { + return item.getContentName(); + } + + if (isNew) { + // 新增操作:生成默认名称 + if (item.getContentType() != null) { + switch (item.getContentType()) { + case 1: + String resourceTypeName = "课件"; + if (item.getResourceType() != null) { + resourceTypeName = getResourceTypeName(item.getResourceType()); + } + return resourceTypeName + "-" + UUID.randomUUID().toString().substring(0, 8); + case 2: + return item.getHomeworkInfo() != null && item.getHomeworkInfo().getName() != null ? + item.getHomeworkInfo().getName() : "未命名作业"; + case 3: + return item.getExamInfo() != null && item.getExamInfo().getTestName() != null ? + item.getExamInfo().getTestName() : "未命名考试"; + case 4: + return "课程评估"; + default: + return "未命名内容"; + } + } + } + + + return null; + } + + /** + * 获取资源类型名称 + */ + private String getResourceTypeName(Integer resType) { + if (resType == null) return "资源"; + switch (resType) { + case 10: return "视频"; + case 20: return "音频"; + case 40: return "文档"; + case 41: return "图文"; + case 50: return "SCORM包"; + case 90: return "外部链接"; + default: return "资源"; + } + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseContentServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseContentServiceImpl.java index 08f16bb5..7b3479f0 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseContentServiceImpl.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseContentServiceImpl.java @@ -11,17 +11,21 @@ import com.xboe.core.cache.IXaskCache; import com.xboe.core.cache.XaskCacheProvider; import com.xboe.core.orm.FieldFilters; import com.xboe.core.orm.IFieldFilter; +import com.xboe.core.orm.IFieldUpdate; import com.xboe.core.orm.UpdateBuilder; import com.xboe.module.course.dao.*; import com.xboe.module.course.dto.CourseContentDto; +import com.xboe.module.course.dto.CourseWareSaveDto; import com.xboe.module.course.dto.SortItem; import com.xboe.module.course.entity.CourseAssess; import com.xboe.module.course.entity.CourseContent; import com.xboe.module.course.entity.CourseExam; import com.xboe.module.course.entity.CourseHomeWork; import com.xboe.module.course.service.ICourseContentService; +import com.xboe.module.course.service.TypeTreeService; import com.xboe.module.exam.dao.ExamPaperDao; import com.xboe.module.exam.vo.TestQuestionVo; +import com.xboe.module.teacher.dao.TeacherDao; import com.xboe.standard.enums.BoedxContentType; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -29,11 +33,24 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import javax.transaction.Transactional; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import com.xboe.module.course.entity.Course; +import com.xboe.module.course.entity.CourseCrowd; +import com.xboe.module.course.entity.CourseTeacher; +import com.xboe.module.teacher.entity.Teacher; +import com.xboe.module.course.dao.CourseDao; +import com.xboe.module.course.dao.CourseCrowdDao; +import com.xboe.module.course.dao.CourseTeacherDao; +import org.apache.commons.collections4.CollectionUtils; +import com.xboe.module.course.dao.*; + + @Slf4j @Service public class CourseContentServiceImpl implements ICourseContentService { @@ -59,59 +76,453 @@ public class CourseContentServiceImpl implements ICourseContentService { @Resource private ExamPaperDao examPaperDao; + @Resource + private CourseDao courseDao; + + @Resource + private CourseTeacherDao courseTeacherDao; + + @Resource + private CourseCrowdDao courseCrowdDao; + + @Resource + private TeacherDao teacherDao; + + @Resource + private TypeTreeService typeTreeService; @Override @Transactional public void saveOrUpdate(CourseContentDto dto) { - CourseContent cc=dto.getContent(); - CourseAssess assess=dto.getAssess(); - CourseExam exam=dto.getExam(); - CourseHomeWork homework=dto.getHomework(); - + CourseContent cc=dto.getContent(); // 获取内容信息 + CourseAssess assess=dto.getAssess(); // 获取评估信息 + CourseExam exam=dto.getExam(); // 获取考试信息 + CourseHomeWork homework=dto.getHomework(); // 获取作业信息 + // 当新增的情况cc.getId() 为空, + // 表示这是一个新创建的对象,而不是更新已有对象 if(StringUtils.isBlank(cc.getId())) { + //新增的情况 + + // 设置课程内容的删除状态标记, + // 采用的是"软删除". + // 重点:表示没有删除 cc.setDeleted(false); + // 检查课程内容的时长是否为空 if(cc.getDuration()==null) { + // 如果时长为空,将其设置为默认值 0 cc.setDuration(0); } - //如果是没有目录的,并具是课程内容 + + + //检查是否为无目录的课程 if(dto.getType()!=null && dto.getType()==10) { + // 如果是无目录的课程,检查是否是表课件,如果是表的课件 if(cc.getSortIndex()==1) { //先删除之前其它的 + // 防止同一课程下出现多个具有相同特殊标识的内容项,比如我之前封面 ccDao.deleteByField("courseId",cc.getCourseId()); } } - ccDao.save(cc); + ccDao.save(cc); // 将信息保存下来 }else { ccDao.update(cc); - cc.setSysVersion(ccDao.getVersion(cc.getId())); + cc.setSysVersion(ccDao.getVersion(cc.getId())); // 保存我的版本 } - //添加或保存其它信息 + //保存其它信息 if(assess!=null) { - assess.setCourseId(cc.getCourseId()); - assessDao.saveOrUpdate(assess); + assess.setCourseId(cc.getCourseId()); // 将我的课程id信息保存进去 + assessDao.saveOrUpdate(assess); // 保存评估信息 } if(exam!=null) { + // 校验选择是否合法 if ((exam.getRandomMode() && !(exam.getQnum() > 0)) || (!exam.getRandomMode() && exam.getQnum() > 0)) { throw new RuntimeException("随机选题处参数错误"); } - exam.setCourseId(cc.getCourseId()); - exam.setContentId(cc.getId()); + exam.setCourseId(cc.getCourseId()); // 设置课程id + exam.setContentId(cc.getId()); // 设置内容id + // 是否是百分之测试 if(exam.getPercentScore()==null) { - exam.setPercentScore(true); + exam.setPercentScore(true); // 如果不是的话,改成我的迷人百分制测试 } - examDao.saveOrUpdate(exam); - exam.setSysVersion(examDao.getVersion(exam.getId())); + examDao.saveOrUpdate(exam); // 将内容进行保存 + exam.setSysVersion(examDao.getVersion(exam.getId())); // 保存我的版本的ID编号 } if(homework!=null) { - homework.setCourseId(cc.getCourseId()); - homework.setContentId(cc.getId()); - homework=homeworkDao.saveOrUpdate(homework); - homework.setSysVersion(homeworkDao.getVersion(homework.getId())); + homework.setCourseId(cc.getCourseId()); // 设置课程id + homework.setContentId(cc.getId()); // 设置内容id + homework=homeworkDao.saveOrUpdate(homework); // 保存作业信息 + homework.setSysVersion(homeworkDao.getVersion(homework.getId())); // 保存我的版本的ID编号 } } - @Override + + @Override + @Transactional + public void saveOrUpdateCourseware(CourseWareSaveDto dto) { + try { + // 1. 基础校验 + validateBaseInfo(dto); + + // 2. 校验分类和归属的层级关系 + validateTypeHierarchy(dto); + + // 3. 核心逻辑:只根据课程ID是否为null判断是新增还是更新 + String courseIdFromDto = dto.getCourse().getId(); + boolean isNew = (courseIdFromDto == null); + String courseId; + + if (isNew) { + // 如果课程ID为null,执行新增操作 + log.info("课程ID为null,执行新增操作"); + courseId = createNewCourse(dto.getCourse()); + } else { + // 如果课程ID不为null,执行更新操作 + log.info("课程ID不为null,执行更新操作,courseId={}", courseIdFromDto); + Course existingCourse = courseDao.get(courseIdFromDto); + if (existingCourse != null) { + courseId = updateExistingCourse(existingCourse, dto.getCourse()); + } else { + // 课程ID不为null但找不到对应课程,仍然创建新课程 + log.info("课程ID '{}' 在数据库中不存在,执行新增操作", courseIdFromDto); + courseId = createNewCourse(dto.getCourse()); + } + } + // 4. 绑定分类(三级) + bindClassification(courseId, dto); + + // 5. 绑定资源归属(三级) + bindResourceOwnership(courseId, dto); + + // 6. 处理观看设置 + if (dto.getDevice() != null) { + courseDao.updateFieldById(courseId, "device", dto.getDevice()); + } else { + // 默认多端可看 + courseDao.updateFieldById(courseId, "device", 3); + } + + // 7. 处理授课教师 + processTeachers(courseId, dto.getTeacherList()); + + // 8. 处理受众列表 + processCrowds(courseId, dto.getCrowdList()); + + log.info("课程课件保存成功,操作类型:{},courseId={}", + isNew ? "新增" : "更新", courseId); + + } catch (RuntimeException e) { + log.error("课程课件保存/更新异常:{}", e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error("课程课件保存/更新系统异常", e); + throw new RuntimeException("课程课件保存/更新失败:" + e.getMessage(), e); + } + } + + /** + * 基础信息校验 + */ + private void validateBaseInfo(CourseWareSaveDto dto) { + if (dto == null) { + throw new RuntimeException("课程课件参数不能为空"); + } + if (StringUtils.isBlank(dto.getCourse().getName())) { + throw new RuntimeException("课程名称不能为空"); + } + if (CollectionUtils.isEmpty(dto.getTeacherList())) { + throw new RuntimeException("至少选择一位授课教师"); + } + if (dto.getSysType1() == null || StringUtils.isBlank(dto.getSysType1().getId())) { + throw new RuntimeException("课程一级分类不能为空"); + } + if (dto.getResOwner1() == null || StringUtils.isBlank(dto.getResOwner1().getId())) { + throw new RuntimeException("资源一级归属不能为空"); + } + if (dto.getDevice() == null || (dto.getDevice() < 1 || dto.getDevice() > 3)) { + throw new RuntimeException("观看设置参数错误,必须是1、2或3"); + } + + // 课程名称长度校验 + if (dto.getCourse().getName().length() > 100) { + throw new RuntimeException("课程名称不能超过100字"); + } + } + + /** + * 校验分类和归属的层级关系 + */ + private void validateTypeHierarchy(CourseWareSaveDto dto) { + // 校验分类层级 + if (dto.getSysType2() != null && StringUtils.isNotBlank(dto.getSysType2().getId())) { + if (dto.getSysType1() == null || StringUtils.isBlank(dto.getSysType1().getId())) { + throw new RuntimeException("选择了二级分类必须有一级分类"); + } + // 验证二级分类的父级是否是一级分类 + if (!typeTreeService.validateTypeHierarchy(dto.getSysType2().getId(), dto.getSysType1().getId())) { + throw new RuntimeException("二级分类的父级必须是一级分类"); + } + } + + if (dto.getSysType3() != null && StringUtils.isNotBlank(dto.getSysType3().getId())) { + if (dto.getSysType2() == null || StringUtils.isBlank(dto.getSysType2().getId())) { + throw new RuntimeException("选择了三级分类必须有二级分类"); + } + if (!typeTreeService.validateTypeHierarchy(dto.getSysType3().getId(), dto.getSysType2().getId())) { + throw new RuntimeException("三级分类的父级必须是二级分类"); + } + } + } + + /** + * 创建新课程 + */ + private String createNewCourse(CourseWareSaveDto.CourseInfo courseInfo) { + Course course = new Course(); + // 设置基础信息 + course.setName(courseInfo.getName()); + course.setValue(courseInfo.getValue()); + course.setSummary(courseInfo.getSummary()); + course.setForUsers(courseInfo.getForUsers()); + course.setTags(courseInfo.getTags()); + course.setCoverImg(courseInfo.getCoverImg()); + course.setType(courseInfo.getType()); + course.setOrgId(courseInfo.getOrgId()); + course.setEnabled(courseInfo.getEnabled()); + course.setIsTop(courseInfo.getIsTop()); + course.setStudyTime(courseInfo.getStudyTime()); + course.setOverview(courseInfo.getOverview()); + course.setOpenCourse(courseInfo.getOpenCourse()); + course.setVisible(courseInfo.getVisible()); + course.setOrderStudy(courseInfo.getOrderStudy()); + course.setKeywords(courseInfo.getKeywords()); + course.setForScene(courseInfo.getForScene()); + course.setCreateFrom(courseInfo.getCreateFrom()); + + // 设置默认值 + setDefaultCourseValues(course); + + courseDao.save(course); + String courseId = course.getId(); + log.info("新增课程成功,courseId={}, 课程名称={}", courseId, course.getName()); + return courseId; + } + + /** + * 更新现有课程 + */ + private String updateExistingCourse(Course existingCourse, CourseWareSaveDto.CourseInfo newCourseData) { + String courseId = existingCourse.getId(); + + // 使用UpdateBuilder更新字段,避免直接修改实体 + List updates = new ArrayList<>(); + + // 更新基础信息 + updates.add(UpdateBuilder.create("name", newCourseData.getName())); + if (newCourseData.getValue() != null) { + updates.add(UpdateBuilder.create("value", newCourseData.getValue())); + } + if (newCourseData.getSummary() != null) { + updates.add(UpdateBuilder.create("summary", newCourseData.getSummary())); + } + if (newCourseData.getForUsers() != null) { + updates.add(UpdateBuilder.create("forUsers", newCourseData.getForUsers())); + } + if (newCourseData.getTags() != null) { + updates.add(UpdateBuilder.create("tags", newCourseData.getTags())); + } + if (newCourseData.getCoverImg() != null) { + updates.add(UpdateBuilder.create("coverImg", newCourseData.getCoverImg())); + } + if (newCourseData.getType() != null) { + updates.add(UpdateBuilder.create("type", newCourseData.getType())); + } + if (newCourseData.getStudyTime() != null) { + updates.add(UpdateBuilder.create("studyTime", newCourseData.getStudyTime())); + } + if (newCourseData.getOverview() != null) { + updates.add(UpdateBuilder.create("overview", newCourseData.getOverview())); + } + + // 版本号递增 +// updates.add(UpdateBuilder.create("sysVersion", existingCourse.getSysVersion() + 1)); + Integer currentVersion = existingCourse.getSysVersion(); + if (currentVersion == null) { + currentVersion = 0; // 如果为null,当做0处理 + } + updates.add(UpdateBuilder.create("sysVersion", currentVersion + 1)); + // 执行更新 + for (IFieldUpdate update : updates) { + courseDao.updateFieldById(courseId, update.getField(), update.getValue()); + } + + + log.info("更新课程成功,courseId={}, 课程名称={}", courseId, newCourseData.getName()); + return courseId; + } + + /** + * 设置课程默认值 + */ + private void setDefaultCourseValues(Course course) { + course.setDeleted(false); + course.setStatus(Course.STATUS_NONE); // 草稿状态 + course.setPublished(false); + course.setEnabled(true); + course.setVisible(true); + course.setErasable(true); + course.setIsTop(false); + course.setOrderStudy(false); + + // 设置统计默认值 + course.setViews(0); + course.setComments(0); + course.setPraises(0); + course.setShares(0); + course.setFavorites(0); + course.setStudys(0); + course.setScore(0f); + course.setTrampleCount(0); + } + + /** + * 绑定分类(三级) + */ + private void bindClassification(String courseId, CourseWareSaveDto dto) { + List updates = new ArrayList<>(); + + if (dto.getSysType1() != null) { + updates.add(UpdateBuilder.create("sysType1", dto.getSysType1().getId())); + } + + if (dto.getSysType2() != null && StringUtils.isNotBlank(dto.getSysType2().getId())) { + updates.add(UpdateBuilder.create("sysType2", dto.getSysType2().getId())); + } else { + updates.add(UpdateBuilder.create("sysType2", null)); + } + + if (dto.getSysType3() != null && StringUtils.isNotBlank(dto.getSysType3().getId())) { + updates.add(UpdateBuilder.create("sysType3", dto.getSysType3().getId())); + } else { + updates.add(UpdateBuilder.create("sysType3", null)); + } + + if (!updates.isEmpty()) { + for (IFieldUpdate update : updates) { + courseDao.updateFieldById(courseId, update.getField(), update.getValue()); + } + + } + } + + /** + * 绑定资源归属(三级) + */ + private void bindResourceOwnership(String courseId, CourseWareSaveDto dto) { + List updates = new ArrayList<>(); + + if (dto.getResOwner1() != null) { + updates.add(UpdateBuilder.create("resOwner1", dto.getResOwner1().getId())); + } + + if (dto.getResOwner2() != null && StringUtils.isNotBlank(dto.getResOwner2().getId())) { + updates.add(UpdateBuilder.create("resOwner2", dto.getResOwner2().getId())); + } else { + updates.add(UpdateBuilder.create("resOwner2", null)); + } + + if (dto.getResOwner3() != null && StringUtils.isNotBlank(dto.getResOwner3().getId())) { + updates.add(UpdateBuilder.create("resOwner3", dto.getResOwner3().getId())); + } else { + updates.add(UpdateBuilder.create("resOwner3", null)); + } + + if (!updates.isEmpty()) { + for (IFieldUpdate update : updates) { + courseDao.updateFieldById(courseId, update.getField(), update.getValue()); + } + } + } + + /** + * 处理授课教师 + */ + private void processTeachers(String courseId, List teacherList) { + // 1. 先删除该课程下旧的教师关联 + courseTeacherDao.deleteByField("courseId", courseId); + + // 2. 遍历新增新的教师关联 + for (CourseTeacher teacher : teacherList) { + // 校验教师ID非空 + if (StringUtils.isBlank(teacher.getTeacherId())) { + throw new RuntimeException("添加失败:教师ID不能为空"); + } + + // 核心:校验教师姓名必须传(null/空串/全空格都判定为未传) +// if (StringUtils.isBlank(teacher.getTeacherName())) { +// throw new RuntimeException("添加失败:教师姓名必须填写,teacherId=" + teacher.getTeacherId()); +// } + + // 3. 查询教师是否存在,不存在则自动新增 + Teacher existTeacher = teacherDao.get(teacher.getTeacherId()); + if (existTeacher == null) { + log.warn("教师ID{}不存在", + teacher.getTeacherId(), teacher.getTeacherName()); + + // 构建新教师实体 + existTeacher = new Teacher(); + existTeacher.setId(teacher.getTeacherId()); + existTeacher.setName(teacher.getTeacherName()); + + // 基础默认值 + existTeacher.setDeleted(false); + existTeacher.setSysVersion(1); + + // 保存新教师 + teacherDao.save(existTeacher); + log.info("自动新增教师成功,teacherId={}, 教师姓名={}", + existTeacher.getId(), existTeacher.getName()); + } + + // 4. 绑定课程ID并保存关联关系(姓名已校验非空) +// teacher.setCourseId(courseId); +// courseTeacherDao.save(teacher); + + teacher.setCourseId(courseId); + + // 清除ID和版本号,避免主键冲突 + + teacher.setId(null); + courseTeacherDao.save(teacher); + } + + log.info("处理授课教师完成,courseId={}, 教师数量={}", courseId, teacherList.size()); + } + + /** + * 处理受众列表 + */ + private void processCrowds(String courseId, List crowdList) { + if (CollectionUtils.isNotEmpty(crowdList)) { + // 先删除旧关联 + courseCrowdDao.deleteByField("courseId", courseId); + + // 新增新关联 + for (CourseCrowd crowd : crowdList) { + if (StringUtils.isBlank(crowd.getGroupId())) { + throw new RuntimeException("受众ID不能为空"); + } + crowd.setCourseId(courseId); // 绑定课程ID + courseCrowdDao.save(crowd); + } + + log.info("处理受众完成,courseId={}, 受众数量={}", courseId, crowdList.size()); + } + } + + + + @Override @Transactional public void delete(String id,int contentType,boolean flag) { if(flag) { diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseContentValidationServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseContentValidationServiceImpl.java new file mode 100644 index 00000000..0d610637 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseContentValidationServiceImpl.java @@ -0,0 +1,537 @@ +package com.xboe.module.course.service.impl; + +import com.xboe.module.course.dto.BatchUploadResponseDto; +import com.xboe.module.course.dto.BatchUploadWithNullDto; +import com.xboe.module.course.dto.CourseResourceUploadDto; +import com.xboe.module.course.service.CourseContentValidationService; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * 课程内容验证 服务实现类 + */ +@Service +public class CourseContentValidationServiceImpl implements CourseContentValidationService { + + /** + * 验证批量上传DTO + */ + @Override + public List validateBatchUpload(BatchUploadWithNullDto dto) { + List errors = new ArrayList<>(); + + if (StringUtils.isBlank(dto.getCourseId())) { + errors.add("课程ID不能为空"); + } + + if (dto.getOperationType() == null) { + errors.add("操作类型不能为空"); + } else if (dto.getOperationType() != 1 ) { + errors.add("操作类型必须是1(新增)"); + } + + if (CollectionUtils.isEmpty(dto.getContentItems())) { + errors.add("内容项列表不能为空"); + return errors; + } + + // 验证每个内容项 + for (int i = 0; i < dto.getContentItems().size(); i++) { + BatchUploadWithNullDto.ContentItem item = dto.getContentItems().get(i); + List itemErrors = validateContentItem(item, dto.getOperationType(), i + 1); + errors.addAll(itemErrors); + } + + return errors; + } + + /** + * 验证内容项 + */ + @Override + public List validateContentItem(BatchUploadWithNullDto.ContentItem item, Integer operationType, int index) { + List errors = new ArrayList<>(); + + // 验证操作类型 + if (operationType == 2) { // 更新操作 + if (StringUtils.isBlank(item.getContentId())) { + errors.add("第" + index + "项:更新操作时内容ID不能为空"); + } + } else if (operationType == 1) { // 新增操作 + // contentType不能为null + if (item.getContentType() == null) { + errors.add("第" + index + "项:新增操作时内容类型不能为空"); + } else { + validateForCreate(errors, item, index); + } + } + + // 验证 + validateNonNullFields(errors, item, index); + + // 验证章节ID + if (StringUtils.isBlank(item.getCsectionId())) { + errors.add("第" + index + "项:章节ID不能为空"); + } + + return errors; + } + + /** + * 验证资源上传DTO + */ + @Override + public String validateResource(CourseResourceUploadDto.CourseFileResource resource) { + Integer resType = resource.getResType(); + + switch (resType) { + case 10: // 视频 + case 20: // 音频 + case 40: // 文档 + case 50: // SCORM包 + if (StringUtils.isBlank(resource.getFileBase64())) { + return getResourceTypeName(resType) + "文件内容不能为空"; + } + if (StringUtils.isBlank(resource.getOriginalFileName())) { + return getResourceTypeName(resType) + "文件名称不能为空"; + } + + if (!isValidFileExtension(resType, resource.getOriginalFileName())) { + return getResourceTypeName(resType) + "文件格式不支持"; + } + break; + case 41: // 图文 + if (resource.getGraphicText() == null) { + return "图文内容不能为空"; + } + if (StringUtils.isBlank(resource.getGraphicText().getTitle())) { + return "图文标题不能为空"; + } + if (StringUtils.isBlank(resource.getGraphicText().getContent())) { + return "图文内容不能为空"; + } + break; + case 90: // 外部链接 + if (resource.getExternalLink() == null) { + return "外部链接内容不能为空"; + } + if (StringUtils.isBlank(resource.getExternalLink().getUrl())) { + return "链接地址不能为空"; + } + if (resource.getExternalLink().getOpenType() != null && + resource.getExternalLink().getOpenType() != 1 && + resource.getExternalLink().getOpenType() != 2) { + return "链接打开方式必须是1(页面嵌入)或2(新窗口打开)"; + } + break; + } + return null; + } + + /** + * 验证资源类型是否有效 + */ + @Override + public boolean isValidResourceType(Integer resType) { + return resType != null && ( + resType == 10 || // 视频 + resType == 20 || // 音频 + resType == 40 || // 文档 + resType == 41 || // 图文 + resType == 50 || // SCORM包 + resType == 90 // 外部链接 + ); + } + + /** + * 验证文件扩展名 + */ + @Override + public boolean isValidFileExtension(Integer resType, String fileName) { + String extension = getFileExtension(fileName); + if (StringUtils.isBlank(extension)) { + return false; + } + + switch (resType) { + case 10: // 视频 + return "mp4".equalsIgnoreCase(extension); + case 20: // 音频 + return "mp3".equalsIgnoreCase(extension); + case 40: // 文档 + return extension.matches("(?i)(doc|docx|xls|xlsx|pptx|txt|pdf)"); + case 50: // SCORM包 + return "zip".equalsIgnoreCase(extension); + default: + return true; + } + } + + /** + * 获取文件扩展名 + */ + @Override + public String getFileExtension(String filename) { + if (StringUtils.isBlank(filename) || !filename.contains(".")) { + return ""; + } + return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase(); + } + + /** + * 获取资源类型名称 + */ + @Override + public String getResourceTypeName(Integer resType) { + switch (resType) { + case 10: return "视频"; + case 20: return "音频"; + case 40: return "文档"; + case 41: return "图文"; + case 50: return "SCORM包"; + case 90: return "外部链接"; + default: return "未知资源"; + } + } + + /** + * 创建一个新的批量上传响应DTO + * @param courseId 课程ID + * @param totalCount 总数 + * @return BatchUploadResponseDto + */ + @Override + public BatchUploadResponseDto createBatchUploadResponse(String courseId, int totalCount) { + BatchUploadResponseDto response = new BatchUploadResponseDto(); + response.setCourseId(courseId); + response.setTotalCount(totalCount); + response.setSuccessCount(0); + response.setFailCount(0); + response.setResults(new ArrayList<>()); + return response; + } + + /** + * 添加结果到批量上传响应DTO + * @param response 响应DTO + * @param result 结果 + */ + @Override + public void addResultToBatchUploadResponse(BatchUploadResponseDto response, BatchUploadResponseDto.UploadResult result) { + if (result.isSuccess()) { + response.setSuccessCount(response.getSuccessCount() + 1); + } else { + response.setFailCount(response.getFailCount() + 1); + } + + response.getResults().add(result); + response.setSuccess(response.getFailCount() == 0); + response.setMessage(String.format("上传完成,成功:%d,失败:%d", response.getSuccessCount(), response.getFailCount())); + } + + // ------------------------ 私有辅助方法(保留在实现类中) ------------------------ + private void validateForCreate(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + switch (item.getContentType()) { + case 1: // 课件 + validateCoursewareForCreate(errors, item, index); + break; + case 2: // 作业 + validateHomeworkForCreate(errors, item, index); + break; + case 3: // 考试 + validateExamForCreate(errors, item, index); + break; + case 4: // 评估 + validateAssessForCreate(errors, item, index); + break; + } + } + + private void validateCoursewareForCreate(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getResourceType() == null) { + errors.add("第" + index + "项:新增课件时资源类型不能为空"); + return; + } + + switch (item.getResourceType()) { + case 10: // 视频 + case 20: // 音频 + case 40: // 文档 + case 50: // SCORM包 + validateFileResourceForCreate(errors, item, index, item.getResourceType()); + break; + case 41: // 图文 + validateGraphicTextForCreate(errors, item, index); + break; + case 90: // 外部链接 + validateExternalLinkForCreate(errors, item, index); + break; + default: + errors.add("第" + index + "项:不支持的资源类型: " + item.getResourceType()); + } + } + + private void validateFileResourceForCreate(List errors, BatchUploadWithNullDto.ContentItem item, int index, Integer resourceType) { + if (item.getFileResource() == null) { + errors.add("第" + index + "项:新增" + getResourceTypeName(resourceType) + "时文件资源不能为空"); + return; + } + + // 文件内容必填 + if (StringUtils.isBlank(item.getFileResource().getFileBase64())) { + errors.add("第" + index + "项:" + getResourceTypeName(resourceType) + "文件内容不能为空"); + } + + // 文件名必填 + if (StringUtils.isBlank(item.getFileResource().getOriginalFileName())) { + errors.add("第" + index + "项:" + getResourceTypeName(resourceType) + "文件名称不能为空"); + } else if (!isValidFileExtension(resourceType, item.getFileResource().getOriginalFileName())) { + errors.add("第" + index + "项:" + getResourceTypeName(resourceType) + "文件格式不支持"); + } + } + + private void validateGraphicTextForCreate(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getGraphicTextResource() == null) { + errors.add("第" + index + "项:新增图文时图文资源不能为空"); + return; + } + + // 标题必填 + if (StringUtils.isBlank(item.getGraphicTextResource().getTitle())) { + errors.add("第" + index + "项:图文标题不能为空"); + } + + // 内容必填 + if (StringUtils.isBlank(item.getGraphicTextResource().getContent())) { + errors.add("第" + index + "项:图文内容不能为空"); + } + } + + private void validateExternalLinkForCreate(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getExternalLinkResource() == null) { + errors.add("第" + index + "项:新增外部链接时链接资源不能为空"); + return; + } + + // 链接地址必填 + if (StringUtils.isBlank(item.getExternalLinkResource().getUrl())) { + errors.add("第" + index + "项:链接地址不能为空"); + } + } + + private void validateHomeworkForCreate(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getHomeworkInfo() == null) { + errors.add("第" + index + "项:新增作业时作业信息不能为空"); + return; + } + + if (StringUtils.isBlank(item.getHomeworkInfo().getName())) { + errors.add("第" + index + "项:作业名称不能为空"); + } + + if (item.getHomeworkInfo().getSubmitMode() == null) { + errors.add("第" + index + "项:提交模式不能为空"); + } + } + + private void validateExamForCreate(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getExamInfo() == null) { + errors.add("第" + index + "项:新增考试时考试信息不能为空"); + return; + } + + if (StringUtils.isBlank(item.getExamInfo().getTestName())) { + errors.add("第" + index + "项:考试名称不能为空"); + } + + if (item.getExamInfo().getPaperType() == null) { + errors.add("第" + index + "项:试卷类型不能为空"); + } + + // 根据试卷类型验证 + if (item.getExamInfo().getPaperType() != null) { + if (item.getExamInfo().getPaperType() == 1 && StringUtils.isBlank(item.getExamInfo().getPaperContent())) { + errors.add("第" + index + "项:自定义试卷内容不能为空"); + } + if (item.getExamInfo().getPaperType() == 2 && StringUtils.isBlank(item.getExamInfo().getPaperId())) { + errors.add("第" + index + "项:试卷ID不能为空"); + } + } + + // 验证随机选题参数 + if (item.getExamInfo().getRandomMode() != null && item.getExamInfo().getRandomMode()) { + if (item.getExamInfo().getQnum() == null || item.getExamInfo().getQnum() <= 0) { + errors.add("第" + index + "项:随机选题模式下,试题数量必须大于0"); + } + if (item.getExamInfo().getQscore() == null || item.getExamInfo().getQscore() <= 0) { + errors.add("第" + index + "项:随机选题模式下,试题分值必须大于0"); + } + } + } + + private void validateAssessForCreate(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getAssessInfo() == null) { + errors.add("第" + index + "项:新增评估时评估信息不能为空"); + return; + } + + if (StringUtils.isBlank(item.getAssessInfo().getQuestion())) { + errors.add("第" + index + "项:评估问题不能为空"); + } + + if (item.getAssessInfo().getQType() == null) { + errors.add("第" + index + "项:问题类型不能为空"); + } + } + + private void validateNonNullFields(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getContentType() != null) { + switch (item.getContentType()) { + case 1: // 课件 + validateCoursewareNonNull(errors, item, index); + break; + case 2: // 作业 + validateHomeworkNonNull(errors, item, index); + break; + case 3: // 考试 + validateExamNonNull(errors, item, index); + break; + case 4: // 评估 + validateAssessNonNull(errors, item, index); + break; + } + } + } + + private void validateCoursewareNonNull(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getResourceType() != null) { + if (!isValidResourceType(item.getResourceType())) { + errors.add("第" + index + "项:不支持的资源类型: " + item.getResourceType()); + return; + } + + switch (item.getResourceType()) { + case 10: // 视频 + case 20: // 音频 + case 40: // 文档 + case 50: // SCORM包 + if (item.getFileResource() != null) { + validateFileResourceNonNull(item.getFileResource(), item.getResourceType(), index, errors); + } + break; + case 41: // 图文 + if (item.getGraphicTextResource() != null) { + validateGraphicTextResourceNonNull(item.getGraphicTextResource(), index, errors); + } + break; + case 90: // 外部链接 + if (item.getExternalLinkResource() != null) { + validateExternalLinkResourceNonNull(item.getExternalLinkResource(), index, errors); + } + break; + } + } + } + + private void validateFileResourceNonNull(BatchUploadWithNullDto.FileResourceInfo resource, Integer resourceType, + int index, List errors) { + if (resource.getFileBase64() != null) { + if (StringUtils.isBlank(resource.getOriginalFileName())) { + errors.add("第" + index + "项:" + getResourceTypeName(resourceType) + "文件名称不能为空"); + } else if (!isValidFileExtension(resourceType, resource.getOriginalFileName())) { + errors.add("第" + index + "项:" + getResourceTypeName(resourceType) + "文件格式不支持"); + } + } + + if (resource.getDevice() != null && (resource.getDevice() < 1 || resource.getDevice() > 3)) { + errors.add("第" + index + "项:设备类型必须是1,2或3"); + } + } + + private void validateGraphicTextResourceNonNull(BatchUploadWithNullDto.GraphicTextResourceInfo resource, + int index, List errors) { + + if (resource.getTitle() != null && StringUtils.isBlank(resource.getTitle())) { + errors.add("第" + index + "项:图文标题不能为空"); + } + + if (resource.getContent() != null && StringUtils.isBlank(resource.getContent())) { + errors.add("第" + index + "项:图文内容不能为空"); + } + + if (resource.getImageBase64() != null && StringUtils.isBlank(resource.getImageName())) { + errors.add("第" + index + "项:图片名称不能为空"); + } + } + + private void validateExternalLinkResourceNonNull(BatchUploadWithNullDto.ExternalLinkResourceInfo resource, + int index, List errors) { + + if (resource.getUrl() != null && StringUtils.isBlank(resource.getUrl())) { + errors.add("第" + index + "项:链接地址不能为空"); + } + + if (resource.getOpenType() != null && + (resource.getOpenType() != 1 && resource.getOpenType() != 2)) { + errors.add("第" + index + "项:链接打开方式必须是1(页面嵌入)或2(新窗口打开)"); + } + } + + private void validateHomeworkNonNull(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getHomeworkInfo() != null) { + + if (item.getHomeworkInfo().getName() != null && StringUtils.isBlank(item.getHomeworkInfo().getName())) { + errors.add("第" + index + "项:作业名称不能为空"); + } + + if (item.getHomeworkInfo().getSubmitMode() != null && + (item.getHomeworkInfo().getSubmitMode() < 1 || item.getHomeworkInfo().getSubmitMode() > 3)) { + errors.add("第" + index + "项:提交模式必须是1,2或3"); + } + } + } + + private void validateExamNonNull(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getExamInfo() != null) { + + if (item.getExamInfo().getTestName() != null && StringUtils.isBlank(item.getExamInfo().getTestName())) { + errors.add("第" + index + "项:考试名称不能为空"); + } + + if (item.getExamInfo().getPaperType() != null) { + if (item.getExamInfo().getPaperType() == 1 && + item.getExamInfo().getPaperContent() != null && + StringUtils.isBlank(item.getExamInfo().getPaperContent())) { + errors.add("第" + index + "项:自定义试卷内容不能为空"); + } + if (item.getExamInfo().getPaperType() == 2 && + item.getExamInfo().getPaperId() != null && + StringUtils.isBlank(item.getExamInfo().getPaperId())) { + errors.add("第" + index + "项:试卷ID不能为空"); + } + } + + if (item.getExamInfo().getRandomMode() != null && item.getExamInfo().getRandomMode()) { + if (item.getExamInfo().getQnum() != null && item.getExamInfo().getQnum() <= 0) { + errors.add("第" + index + "项:随机选题模式下,试题数量必须大于0"); + } + if (item.getExamInfo().getQscore() != null && item.getExamInfo().getQscore() <= 0) { + errors.add("第" + index + "项:随机选题模式下,试题分值必须大于0"); + } + } + } + } + + private void validateAssessNonNull(List errors, BatchUploadWithNullDto.ContentItem item, int index) { + if (item.getAssessInfo() != null) { + + if (item.getAssessInfo().getQuestion() != null && StringUtils.isBlank(item.getAssessInfo().getQuestion())) { + errors.add("第" + index + "项:评估问题不能为空"); + } + } + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseResourceServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseResourceServiceImpl.java new file mode 100644 index 00000000..41610f26 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/CourseResourceServiceImpl.java @@ -0,0 +1,435 @@ +package com.xboe.module.course.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xboe.common.OrderCondition; +import com.xboe.core.orm.FieldFilters; +import com.xboe.module.course.dao.CourseFileDao; +import com.xboe.module.course.dto.CourseResourceUploadDto; +import com.xboe.module.course.entity.CourseFile; +import com.xboe.module.course.service.CourseContentValidationService; +import com.xboe.module.course.service.CourseResourceService; + +import com.xboe.module.util.FileUploadUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.*; + +@Slf4j +@Service +public class CourseResourceServiceImpl implements CourseResourceService { + + @Resource + private CourseFileDao courseFileDao; + + @Resource + private FileUploadUtil fileUploadUtil; + + @Resource + private CourseContentValidationService courseContentValidationService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + // 资源类型映射 + private static final Map RESOURCE_TYPE_EXTENSIONS = new HashMap<>(); + private static final Map RESOURCE_TYPE_NAMES = new HashMap<>(); + + static { + // 文件扩展名映射 + RESOURCE_TYPE_EXTENSIONS.put(10, new String[]{"mp4"}); // 视频 + RESOURCE_TYPE_EXTENSIONS.put(20, new String[]{"mp3"}); // 音频 + RESOURCE_TYPE_EXTENSIONS.put(40, new String[]{"doc", "docx", "xls", "xlsx", "pptx", "txt", "pdf"}); // 文档 + RESOURCE_TYPE_EXTENSIONS.put(50, new String[]{"zip"}); // SCORM包 + + // 资源类型名称映射 + RESOURCE_TYPE_NAMES.put(10, "视频"); + RESOURCE_TYPE_NAMES.put(20, "音频"); + RESOURCE_TYPE_NAMES.put(40, "文档"); + RESOURCE_TYPE_NAMES.put(41, "图文"); + RESOURCE_TYPE_NAMES.put(50, "SCORM包"); + RESOURCE_TYPE_NAMES.put(90, "外部链接"); + } + + @Override + @Transactional + public void processCourseResources(String courseId, + List resources, + String orgId, String orgName, + String resOwner1, String resOwner2, String resOwner3, + String ownership1, String ownership2, String ownership3) { + + if (resources == null || resources.isEmpty()) { + log.info("课程ID={} 没有需要处理的资源", courseId); + return; + } + + log.info("开始处理课程资源,课程ID={},资源数量={}", courseId, resources.size()); + + int successCount = 0; + int failCount = 0; + + for (CourseResourceUploadDto.CourseFileResource resource : resources) { + try { + processSingleResource(courseId, resource, orgId, orgName, + resOwner1, resOwner2, resOwner3, + ownership1, ownership2, ownership3); + successCount++; + log.info("处理资源成功: 课程ID={}, 资源名称={}, 类型={}", + courseId, resource.getName(), resource.getResType()); + } catch (Exception e) { + failCount++; + log.error("处理资源失败: 课程ID={}, 资源名称={}, 类型={}, 错误: {}", + courseId, resource.getName(), resource.getResType(), e.getMessage(), e); + // 继续处理其他资源 + } + } + + log.info("资源处理完成: 课程ID={}, 成功={}, 失败={}", courseId, successCount, failCount); + } + + /** + * 处理单个资源 + */ + private void processSingleResource(String courseId, + CourseResourceUploadDto.CourseFileResource resource, + String orgId, String orgName, + String resOwner1, String resOwner2, String resOwner3, + String ownership1, String ownership2, String ownership3) throws IOException { + + // 验证资源 + validateResource(resource); + + // 创建CourseFile实体 + CourseFile courseFile = new CourseFile(); + courseFile.setName(resource.getName()); + courseFile.setCourseId(courseId); + courseFile.setOrgId(orgId); + courseFile.setOrgName(orgName); + courseFile.setResType(resource.getResType()); + courseFile.setResOwner1(resOwner1); + courseFile.setResOwner2(resOwner2); + courseFile.setResOwner3(resOwner3); + courseFile.setOwnership1(ownership1); + courseFile.setOwnership2(ownership2); + courseFile.setOwnership3(ownership3); + courseFile.setDown(resource.getDown() != null ? resource.getDown() : false); + courseFile.setRemark(resource.getRemark()); + courseFile.setDevice(resource.getDevice() != null ? resource.getDevice() : 3); // 默认多端可见 + courseFile.setDecoder(resource.getDecoder()); + courseFile.setVideoWidth(resource.getVideoWidth()); + courseFile.setVideoHeight(resource.getVideoHeight()); + courseFile.setConverStatus(resource.getConverStatus() != null ? resource.getConverStatus() : 0); + courseFile.setConverError(resource.getConverError()); + + // 根据资源类型处理 + Integer resType = resource.getResType(); + if (resType == 10 || resType == 20 || resType == 40 || resType == 50) { + // 文件资源(视频、音频、文档、SCORM包) + processFileResource(courseFile, resource); + } else if (resType == 41) { + // 图文资源 + processGraphicTextResource(courseFile, resource); + } else if (resType == 90) { + // 外部链接资源 + processExternalLinkResource(courseFile, resource); + } else { + throw new IllegalArgumentException("不支持的资源类型: " + resType); + } + + // 保存到数据库 + courseFileDao.save(courseFile); + log.info("资源保存到数据库成功: ID={}, 名称={}, 类型={}", + courseFile.getId(), courseFile.getName(), courseFile.getResType()); + } + + /** + * 处理文件资源(视频、音频、文档、SCORM包通用方法) + */ + private void processFileResource(CourseFile courseFile, + CourseResourceUploadDto.CourseFileResource resource) throws IOException { + + Integer resType = resource.getResType(); + String resourceTypeName = RESOURCE_TYPE_NAMES.get(resType); + String[] allowedExtensions = RESOURCE_TYPE_EXTENSIONS.get(resType); + + // 验证文件 + if (StringUtils.isBlank(resource.getFileBase64())) { + throw new IllegalArgumentException(resourceTypeName + "文件内容不能为空"); + } + + String originalFileName = StringUtils.isNotBlank(resource.getOriginalFileName()) + ? resource.getOriginalFileName() + : getDefaultFileName(resType); + + // 验证文件类型 + if (!fileUploadUtil.validateFileType(originalFileName, allowedExtensions)) { + throw new IllegalArgumentException(resourceTypeName + "文件只支持: " + + String.join(", ", allowedExtensions)); + } + + // 验证文件大小 + validateFileSize(resource.getFileBase64(), resType); + + // 保存文件 + FileUploadUtil.FileSaveResult saveResult = fileUploadUtil.saveBase64File( + resource.getFileBase64(), originalFileName); + + // 设置文件信息 + courseFile.setFileName(saveResult.getFileName()); + courseFile.setFileType(fileUploadUtil.getFileExtension(originalFileName)); + courseFile.setFileSize((int) (saveResult.getFileSize() / 1024)); // KB + courseFile.setFilePath(saveResult.getFilePath()); + courseFile.setPreviewFilePath(saveResult.getFileUrl()); + + // 设置时长(视频/音频) + if (resType == 10 || resType == 20) { + courseFile.setDuration(resource.getDuration() != null ? resource.getDuration() : 0); + } + + // 视频特定设置 + if (resType == 10) { + courseFile.setDecoder(StringUtils.isNotBlank(resource.getDecoder()) ? resource.getDecoder() : "h264"); + courseFile.setConverStatus(0); // 视频可能需要转码 + } else { + courseFile.setConverStatus(2); // 其他文件不需要转化 + } + } + + /** + * 处理图文资源 + */ + private void processGraphicTextResource(CourseFile courseFile, + CourseResourceUploadDto.CourseFileResource resource) throws IOException { + + // 验证图文内容 + if (resource.getGraphicText() == null) { + throw new IllegalArgumentException("图文内容不能为空"); + } + + CourseResourceUploadDto.GraphicTextContent graphicText = resource.getGraphicText(); + if (StringUtils.isBlank(graphicText.getTitle())) { + throw new IllegalArgumentException("图文标题不能为空"); + } + + if (StringUtils.isBlank(graphicText.getContent())) { + throw new IllegalArgumentException("图文内容不能为空"); + } + + // 处理图片(如果有) + String imageUrl = null; + String imagePath = null; + if (StringUtils.isNotBlank(graphicText.getImageBase64())) { + try { + String imageName = StringUtils.isNotBlank(graphicText.getImageName()) + ? graphicText.getImageName() + : "image_" + System.currentTimeMillis() + ".jpg"; + + FileUploadUtil.FileSaveResult imageResult = fileUploadUtil.saveBase64File( + graphicText.getImageBase64(), imageName); + imageUrl = imageResult.getFileUrl(); + imagePath = imageResult.getFilePath(); + } catch (Exception e) { + log.warn("处理图文图片失败,继续处理其他内容", e); + } + } + + // 构建图文内容JSON + Map contentMap = new HashMap<>(); + contentMap.put("title", graphicText.getTitle()); + contentMap.put("content", graphicText.getContent()); + contentMap.put("format", graphicText.getFormat()); + contentMap.put("fontSize", graphicText.getFontSize()); + contentMap.put("lineHeight", graphicText.getLineHeight()); + contentMap.put("textAlign", graphicText.getTextAlign()); + contentMap.put("imageUrl", imageUrl); + contentMap.put("imageName", graphicText.getImageName()); + + String contentJson = objectMapper.writeValueAsString(contentMap); + + // 设置文件信息 + courseFile.setFileName("graphic_text.json"); + courseFile.setFileType("json"); + courseFile.setFileSize((int) (contentJson.length() / 1024)); // KB + courseFile.setContent(contentJson); + if (imageUrl != null) { + courseFile.setPreviewFilePath(imageUrl); + } + if (imagePath != null) { + courseFile.setFilePath(imagePath); + } else { + courseFile.setFilePath("graphic_text/" + courseFile.getId() + ".json"); + } + courseFile.setConverStatus(2); // 图文不需要转化 + } + + /** + * 处理外部链接资源 + */ + private void processExternalLinkResource(CourseFile courseFile, + CourseResourceUploadDto.CourseFileResource resource) throws IOException { + + // 验证链接内容 + if (resource.getExternalLink() == null) { + throw new IllegalArgumentException("外部链接内容不能为空"); + } + + CourseResourceUploadDto.ExternalLinkContent externalLink = resource.getExternalLink(); + if (StringUtils.isBlank(externalLink.getUrl())) { + throw new IllegalArgumentException("链接地址不能为空"); + } + + // 构建链接内容JSON + Map contentMap = new HashMap<>(); + contentMap.put("url", externalLink.getUrl()); + contentMap.put("openType", externalLink.getOpenType() != null ? externalLink.getOpenType() : 2); + contentMap.put("title", externalLink.getTitle()); + contentMap.put("description", externalLink.getDescription()); + + String contentJson = objectMapper.writeValueAsString(contentMap); + + // 设置文件信息 + courseFile.setFileName("external_link.json"); + courseFile.setFileType("json"); + courseFile.setFileSize((int) (contentJson.length() / 1024)); // KB + courseFile.setContent(contentJson); + courseFile.setFilePath("external_link/" + courseFile.getId() + ".json"); + courseFile.setConverStatus(2); // 链接不需要转化 + } + + /** + * 验证资源 + */ + private void validateResource(CourseResourceUploadDto.CourseFileResource resource) { + if (resource == null) { + throw new IllegalArgumentException("资源不能为空"); + } + + if (resource.getResType() == null) { + throw new IllegalArgumentException("资源类型不能为空"); + } + + if (StringUtils.isBlank(resource.getName())) { + throw new IllegalArgumentException("资源名称不能为空"); + } + + // 使用服务层的验证方法替代DTO中的静态方法 + String validationResult = courseContentValidationService.validateResource(resource); + if (validationResult != null) { + throw new IllegalArgumentException(validationResult); + } + } + + /** + * 验证文件大小 + */ + private void validateFileSize(String base64Content, Integer resType) { + if (StringUtils.isBlank(base64Content)) { + return; + } + + try { + // 估算文件大小(Base64编码比原始大约33%) + int base64Length = base64Content.length(); + if (base64Content.contains(",")) { + base64Length = base64Content.split(",")[1].length(); + } + + // Base64解码后的实际大小约为 base64Length * 3 / 4 + long estimatedSize = (long) base64Length * 3 / 4; + + // 根据类型设置限制 + long maxSize; + switch (resType) { + case 10: // 视频 + case 40: // 文档 + case 50: // SCORM包 + maxSize = 1024L * 1024 * 1024; // 1GB + break; + case 20: // 音频 + maxSize = 16L * 1024 * 1024; // 16MB + break; + default: + maxSize = 100L * 1024 * 1024; // 100MB 默认 + } + + if (estimatedSize > maxSize) { + throw new IllegalArgumentException(String.format( + "文件大小超过限制,估计大小: %.2fMB, 最大限制: %.2fMB", + estimatedSize / (1024.0 * 1024.0), + maxSize / (1024.0 * 1024.0))); + } + } catch (Exception e) { + log.warn("验证文件大小失败", e); + } + } + + /** + * 获取默认文件名 + */ + private String getDefaultFileName(Integer resType) { + switch (resType) { + case 10: return "video.mp4"; + case 20: return "audio.mp3"; + case 40: return "document.pdf"; + case 50: return "scorm.zip"; + default: return "file.bin"; + } + } + + @Override + public List getCourseResources(String courseId) { + try { + List resources = courseFileDao.findList( + OrderCondition.asc("sysCreateTime"), + FieldFilters.eq("courseId", courseId), + FieldFilters.eq("deleted", false) + ); + log.info("获取课程资源成功: courseId={}, 数量={}", courseId, resources.size()); + return resources; + } catch (Exception e) { + log.error("获取课程资源失败: courseId={}", courseId, e); + throw new RuntimeException("获取课程资源失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public void deleteCourseResources(String courseId) { + try { + List resources = getCourseResources(courseId); + for (CourseFile resource : resources) { + deleteResource(resource.getId()); + } + log.info("删除课程资源成功: courseId={}, 数量={}", courseId, resources.size()); + } catch (Exception e) { + log.error("删除课程资源失败: courseId={}", courseId, e); + throw new RuntimeException("删除课程资源失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public void deleteResource(String resourceId) { + try { + CourseFile resource = courseFileDao.get(resourceId); + if (resource != null) { + // 标记为删除 + resource.setDeleted(true); + courseFileDao.update(resource); + + // 删除物理文件 + if (StringUtils.isNotBlank(resource.getFilePath())) { + fileUploadUtil.deleteFile(resource.getFilePath()); + } + log.info("删除资源成功: resourceId={}, 名称={}", resourceId, resource.getName()); + } + } catch (Exception e) { + log.error("删除资源失败: resourceId={}", resourceId, e); + throw new RuntimeException("删除资源失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/TypeTreeServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/TypeTreeServiceImpl.java new file mode 100644 index 00000000..2f9ed5c5 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/service/impl/TypeTreeServiceImpl.java @@ -0,0 +1,275 @@ +package com.xboe.module.course.service.impl; + +import com.xboe.common.OrderCondition; +import com.xboe.core.orm.FieldFilters; +import com.xboe.module.course.vo.TypeTreeVo; +import com.xboe.module.course.service.TypeTreeService; +import com.xboe.module.type.dao.TypeDao; +import com.xboe.module.type.entity.Type; +import com.xboe.system.organization.dao.OrganizationDao; +import com.xboe.system.organization.entity.Organization; +import com.xboe.system.organization.vo.OrganizationVo; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class TypeTreeServiceImpl implements TypeTreeService { + + @Resource + private TypeDao typeDao; + + @Resource + private OrganizationDao organizationDao; + + @Override + public List getCourseTypeTree() { + try { + // 查询课程相关的分类(直接用数据库列名sys_res_type) + List allTypes = typeDao.findList( + OrderCondition.asc("orderIndex"), + FieldFilters.eq("sys_res_type", 1), // 关键:用数据库原生列名 + FieldFilters.eq("status", 1), + FieldFilters.eq("deleted", false) + ); + + return buildTypeTree(allTypes); + } catch (Exception e) { + log.error("获取课程分类树失败", e); + throw new RuntimeException("获取课程分类树失败:" + e.getMessage()); + } + } + + @Override + public List getResourceOwnerTree() { + try { + // 查询组织机构树 + List allOrgs = organizationDao.findList( + OrderCondition.asc("code"), + FieldFilters.eq("status", 1), + FieldFilters.eq("deleted", false) + ); + + return buildOrgTree(allOrgs); + } catch (Exception e) { + log.error("获取资源归属树失败", e); + throw new RuntimeException("获取资源归属树失败:" + e.getMessage()); + } + } + + @Override + public List getChildTypes(String parentId) { + try { + List childTypes = typeDao.findList( + OrderCondition.asc("orderIndex"), + FieldFilters.eq("parent_id", parentId), // 关键:用数据库原生列名 + FieldFilters.eq("status", 1), + FieldFilters.eq("deleted", false), + FieldFilters.eq("sys_res_type", 1) // 关键:用数据库原生列名 + ); + + return childTypes.stream().map(this::convertTypeToTreeVo).collect(Collectors.toList()); + } catch (Exception e) { + log.error("获取子分类失败,parentId={}", parentId, e); + throw new RuntimeException("获取子分类失败:" + e.getMessage()); + } + } + + @Override + public List getChildOrgs(String parentId) { + try { + List childOrgs = organizationDao.findList( + OrderCondition.asc("code"), + FieldFilters.eq("parentId", parentId), + FieldFilters.eq("status", 1), + FieldFilters.eq("deleted", false) + ); + + return childOrgs.stream().map(this::convertOrgToVo).collect(Collectors.toList()); + } catch (Exception e) { + log.error("获取子组织失败,parentId={}", parentId, e); + throw new RuntimeException("获取子组织失败:" + e.getMessage()); + } + } + + @Override + public boolean validateTypeHierarchy(String childId, String parentId) { + try { + // 1. 非空校验 + if (StringUtils.isBlank(childId) || StringUtils.isBlank(parentId)) { + log.warn("分类层级校验失败:子分类ID[{}]或父分类ID[{}]为空", childId, parentId); + return false; + } + + // 确认传入的ID + log.info("开始校验分类层级:子分类ID={}, 父分类ID={}", childId, parentId); + + // 2. 查询子分类 + List childTypeList = typeDao.findList( + FieldFilters.eq("id", childId), + FieldFilters.eq("sys_res_type", 1), // 数据库列名:sys_res_type + FieldFilters.eq("status", 1), + FieldFilters.eq("deleted", false) + ); + if (childTypeList == null || childTypeList.isEmpty()) { + log.warn("分类层级校验失败:子分类ID={} 不存在(或非课程类型/未启用/已删除)", childId); + // 查询所有课程类型分类 + List allCourseTypes = typeDao.findList(FieldFilters.eq("sys_res_type", 1)); + log.info("当前数据库中课程类型分类列表:{}", + allCourseTypes.stream().map(Type::getId).collect(Collectors.toList())); + return false; + } + Type childType = childTypeList.get(0); + log.info("查询到子分类信息:ID={}, 父ID={}, 资源类型={}, 状态={}, 删除标识={}", + childType.getId(), childType.getParentId(), childType.getSysResType(), + childType.getStatus(), childType.getDeleted()); + + // 3. 查询父分类 + List parentTypeList = typeDao.findList( + FieldFilters.eq("id", parentId), + FieldFilters.eq("sys_res_type", 1), // 数据库列名:sys_res_type + FieldFilters.eq("status", 1), + FieldFilters.eq("deleted", false) + ); + if (parentTypeList == null || parentTypeList.isEmpty()) { + log.warn("分类层级校验失败:父分类ID={} 不存在(或非课程类型/未启用/已删除)", parentId); + return false; + } + Type parentType = parentTypeList.get(0); + log.info("查询到父分类信息:ID={}, 父ID={}, 资源类型={}, 状态={}, 删除标识={}", + parentType.getId(), parentType.getParentId(), parentType.getSysResType(), + parentType.getStatus(), parentType.getDeleted()); + + // 4. 验证父分类是一级分类 + if (!StringUtils.isBlank(parentType.getParentId())) { + String parentParentId = parentType.getParentId(); + // 一级分类 + boolean isRootLevel = "0".equals(parentParentId) || "-1".equals(parentParentId); + if (!isRootLevel) { + log.warn("分类层级校验失败:父分类ID={} 不是一级分类(其parentId={})", + parentId, parentType.getParentId()); + return false; + } + log.info("父分类是一级分类:parentId={}, parentParentId={}", parentId, parentParentId); + } else { + log.info("父分类是一级分类:parentId={}, parentParentId为空", parentId); + } + + // 5. 验证 + boolean isParentMatch = parentId.equals(childType.getParentId()); + if (!isParentMatch) { + log.warn("分类层级校验失败:子分类ID={} 的父ID={} 与传入的父ID={} 不匹配", + childId, childType.getParentId(), parentId); + } + + log.info("分类层级校验完成:子分类ID={}, 父分类ID={}, 校验结果={}", childId, parentId, isParentMatch); + return isParentMatch; + } catch (Exception e) { + log.error("验证分类层级关系异常, childId={}, parentId={}", childId, parentId, e); + return false; + } + } + + /** + * 构建分类树 + */ + private List buildTypeTree(List allTypes) { + Map voMap = new HashMap<>(); + List rootNodes = new ArrayList<>(); + + // 第一遍:创建所有节点 + for (Type type : allTypes) { + TypeTreeVo vo = convertTypeToTreeVo(type); + voMap.put(type.getId(), vo); + } + + // 第二遍:建立父子关系 + for (Type type : allTypes) { + TypeTreeVo vo = voMap.get(type.getId()); + if (StringUtils.isBlank(type.getParentId()) || + "0".equals(type.getParentId()) || + "-1".equals(type.getParentId())) { + rootNodes.add(vo); + } else { + TypeTreeVo parent = voMap.get(type.getParentId()); + if (parent != null) { + if (parent.getChildren() == null) { + parent.setChildren(new ArrayList<>()); + } + parent.getChildren().add(vo); + } + } + } + + return rootNodes; + } + + /** + * 构建组织机构树 + */ + private List buildOrgTree(List allOrgs) { + Map voMap = new HashMap<>(); + List rootNodes = new ArrayList<>(); + + // 第一遍:创建所有节点 + for (Organization org : allOrgs) { + OrganizationVo vo = convertOrgToVo(org); + voMap.put(org.getId(), vo); + } + + // 第二遍:建立父子关系 + for (Organization org : allOrgs) { + OrganizationVo vo = voMap.get(org.getId()); + if (StringUtils.isBlank(org.getParentId())) { + rootNodes.add(vo); + } else { + OrganizationVo parent = voMap.get(org.getParentId()); + if (parent != null) { + if (parent.getChildren() == null) { + parent.setChildren(new ArrayList<>()); + } + parent.getChildren().add(vo); + } + } + } + + return rootNodes; + } + + /** + * 转TypeTreeVo + */ + private TypeTreeVo convertTypeToTreeVo(Type type) { + TypeTreeVo vo = new TypeTreeVo(); + vo.setValue(type.getId()); + vo.setLabel(type.getName()); + vo.setParentId(type.getParentId()); + vo.setOrderIndex(type.getOrderIndex()); + vo.setStatus(type.getStatus()); + vo.setSysResType(type.getSysResType()); + return vo; + } + + /** + * 转OrganizationVo + */ + private OrganizationVo convertOrgToVo(Organization org) { + OrganizationVo vo = new OrganizationVo(); + vo.setId(org.getId()); + vo.setName(org.getName()); + vo.setCode(org.getCode()); + vo.setParentId(org.getParentId()); + vo.setStatus(org.getStatus()); + vo.setNamePath(org.getNamePath()); + vo.setDescription(org.getDescription()); + return vo; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/course/vo/TypeTreeVo.java b/servers/boe-server-all/src/main/java/com/xboe/module/course/vo/TypeTreeVo.java new file mode 100644 index 00000000..eb9d1f4f --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/course/vo/TypeTreeVo.java @@ -0,0 +1,27 @@ +package com.xboe.module.course.vo; + +import lombok.Data; + +import java.util.List; + +/** + * 分类树VO(用于前端级联选择器) + */ +@Data +public class TypeTreeVo { + private String value; + private String label; + private String parentId; + private Integer orderIndex; + private Integer status; + private Integer sysResType; + private List children; + + public TypeTreeVo() {} + + public TypeTreeVo(String value, String label, String parentId) { + this.value = value; + this.label = label; + this.parentId = parentId; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/teacher/api/TeacherApi.java b/servers/boe-server-all/src/main/java/com/xboe/module/teacher/api/TeacherApi.java index 83318ef4..84b64248 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/teacher/api/TeacherApi.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/teacher/api/TeacherApi.java @@ -333,7 +333,7 @@ public class TeacherApi extends ApiBaseController { OutputStream = response.getOutputStream(); LinkedHashMap map = new LinkedHashMap<>(); map.put("姓名","name"); - map.put("工号","userNo"); + map.put("工号","userNo"); // 重点 map.put("部门","departName"); map.put("创建时间","sysCreateTime"); map.put("修改时间","sysUpdateTime"); diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/util/FileUploadUtil.java b/servers/boe-server-all/src/main/java/com/xboe/module/util/FileUploadUtil.java new file mode 100644 index 00000000..ff0f0033 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/util/FileUploadUtil.java @@ -0,0 +1,223 @@ +package com.xboe.module.util; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.UUID; + +@Slf4j +@Component +public class FileUploadUtil { + + @Value("${xboe.upload.file.save_path:D:/file_save}") + private String uploadSavePath; + + @Value("${xboe.upload.file.http_path:http://192.168.0.253/upload}") + private String uploadHttpPath; + + @Value("${xboe.upload.file.temp_path:D:/temp}") + private String uploadTempPath; + + /** + * 保存Base64文件到磁盘 + */ + public FileSaveResult saveBase64File(String base64Content, String originalFileName) throws IOException { + if (StringUtils.isBlank(base64Content)) { + throw new IllegalArgumentException("文件内容不能为空"); + } + + log.info("开始保存文件,原始文件名: {}, 保存路径: {}", originalFileName, uploadSavePath); + + // 解码Base64 + byte[] fileBytes; + try { + if (base64Content.contains(",")) { + // 去除data:image/png;base64,前缀 + base64Content = base64Content.split(",")[1]; + } + fileBytes = Base64.getDecoder().decode(base64Content); + } catch (IllegalArgumentException e) { + throw new IOException("Base64解码失败: " + e.getMessage()); + } + + // 生成文件名 + String fileExtension = getFileExtension(originalFileName); + String uuid = UUID.randomUUID().toString().replace("-", ""); + String fileName = uuid + (StringUtils.isNotBlank(fileExtension) ? "." + fileExtension : ""); + + // 按日期组织目录 + String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + String relativePath = datePath + "/" + fileName; + + // 处理Windows路径分隔符 + Path fullPath = Paths.get(uploadSavePath, datePath, fileName); + + // 创建目录 + Path directory = Paths.get(uploadSavePath, datePath); + if (!Files.exists(directory)) { + Files.createDirectories(directory); + log.info("创建目录: {}", directory.toString()); + } + + // 保存文件 + try (FileOutputStream fos = new FileOutputStream(fullPath.toFile())) { + fos.write(fileBytes); + } + + // 构建访问URL + String fileUrl = uploadHttpPath + "/" + relativePath.replace("\\", "/"); + + FileSaveResult result = new FileSaveResult(); + result.setFilePath(relativePath); // 相对路径,用于数据库存储 + result.setFileUrl(fileUrl); // 完整的HTTP访问URL + result.setFileName(fileName); // 保存后的文件名 + result.setOriginalFileName(originalFileName); // 原始文件名 + result.setFileSize(fileBytes.length); // 文件大小(字节) + + log.info("文件保存成功: 原始文件={}, 保存文件={}, 大小={}字节, 路径={}", + originalFileName, fileName, fileBytes.length, relativePath); + + return result; + } + + /** + * 获取文件扩展名 + */ + public String getFileExtension(String filename) { + if (StringUtils.isBlank(filename) || !filename.contains(".")) { + return ""; + } + return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase(); + } + + /** + * 验证文件类型 + */ + public boolean validateFileType(String fileName, String[] allowedExtensions) { + String extension = getFileExtension(fileName); + if (StringUtils.isBlank(extension)) { + return false; + } + + for (String allowed : allowedExtensions) { + if (allowed.equalsIgnoreCase(extension)) { + return true; + } + } + return false; + } + + /** + * 获取支持的文档类型 + */ + public String[] getDocumentExtensions() { + return new String[]{"doc", "docx", "xls", "xlsx", "pptx", "txt", "pdf"}; + } + + /** + * 删除文件 + */ + public boolean deleteFile(String filePath) { + if (StringUtils.isBlank(filePath)) { + return false; + } + + try { + Path fullPath = Paths.get(uploadSavePath, filePath); + File file = fullPath.toFile(); + if (file.exists()) { + boolean deleted = file.delete(); + log.info("删除文件: {}, 结果: {}", fullPath.toString(), deleted); + return deleted; + } + log.warn("文件不存在,无法删除: {}", fullPath.toString()); + return false; + } catch (Exception e) { + log.error("删除文件失败: {}", filePath, e); + return false; + } + } + + /** + * 获取文件完整路径 + */ + public String getFullPath(String relativePath) { + if (StringUtils.isBlank(relativePath)) { + return ""; + } + return Paths.get(uploadSavePath, relativePath).toString(); + } + + /** + * 检查文件是否存在 + */ + public boolean fileExists(String relativePath) { + if (StringUtils.isBlank(relativePath)) { + return false; + } + String fullPath = getFullPath(relativePath); + File file = new File(fullPath); + return file.exists(); + } + + @PostConstruct + public void init() { + try { + // 确保上传目录存在 + File saveDir = new File(uploadSavePath); + if (!saveDir.exists()) { + boolean created = saveDir.mkdirs(); + log.info("创建上传目录: {}, 结果: {}", uploadSavePath, created); + } else { + log.info("上传目录已存在: {}", uploadSavePath); + } + + // 确保临时目录存在 + File tempDir = new File(uploadTempPath); + if (!tempDir.exists()) { + boolean created = tempDir.mkdirs(); + log.info("创建临时目录: {}, 结果: {}", uploadTempPath, created); + } + + log.info("文件上传配置初始化完成: savePath={}, httpPath={}, tempPath={}", + uploadSavePath, uploadHttpPath, uploadTempPath); + + // 检查目录权限 + if (!saveDir.canWrite()) { + log.error("上传目录不可写: {}", uploadSavePath); + } + + if (!tempDir.canWrite()) { + log.error("临时目录不可写: {}", uploadTempPath); + } + + } catch (Exception e) { + log.error("初始化上传目录失败", e); + } + } + + /** + * 文件保存结果 + */ + @Data + public static class FileSaveResult { + private String filePath; // 相对路径,用于数据库存储 + private String fileUrl; // 完整的HTTP访问URL + private String fileName; // 保存后的文件名 + private String originalFileName; // 原始文件名 + private long fileSize; // 文件大小(字节) + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/resources/application-dev.yml b/servers/boe-server-all/src/main/resources/application-dev.yml index 92cfb4a8..b6494b0f 100644 --- a/servers/boe-server-all/src/main/resources/application-dev.yml +++ b/servers/boe-server-all/src/main/resources/application-dev.yml @@ -16,12 +16,15 @@ spring: cloud: nacos: discovery: - server-addr: 192.168.0.253:8848 + server-addr: 127.0.0.1:8848 + # server-addr: 192.168.0.253:8848 config: - server-addr: 192.168.0.253:8848 + server-addr: 127.0.0.1:8848 + # server-addr: 192.168.0.253:8848 redis: database: 1 - host: 192.168.0.253 + host: 127.0.0.1 + # host: 192.168.0.253 password: boe@123 port: 6379 jpa: @@ -29,9 +32,9 @@ spring: ddl-auto: none datasource: driverClassName: com.mysql.jdbc.Driver - url: jdbc:mysql://192.168.0.253:3306/boe_base?useSSL=false&useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull - username: root - password: boe#1234A + url: jdbc:mysql://rm-hp3cpkk0u50q90eu9vo.mysql.huhehaote.rds.aliyuncs.com:3306/ebiz_doc_manage_bpic?characterEncoding=utf8&useUnicode=true&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true + username: ebiz_ai + password: ebiz_ai123 type: com.zaxxer.hikari.HikariDataSource hikari: auto-commit: true @@ -92,18 +95,19 @@ xboe: ai-api-code: 30800 case-knowledge-id: de2e006e-82fb-4ace-8813-f25c316be4ff file-upload-callback-url: http://192.168.0.253:9090/xboe/m/boe/caseDocumentLog/uploadCallback - alert-email-recipients: + alert-email-recipients: - liu.zixi@ebiz-digits.com xxl: job: - accessToken: 65ddc683-22f5-83b4-de3a-3c97a0a29af0 + # accessToken: 65ddc683-22f5-83b4-de3a-3c97a0a29af0 + accessToken: default_token admin: - addresses: http://192.168.0.253/jobAdmin + addresses: http://127.0.0.1:8080/xxl-job-admin executor: appname: java-servers-job-api port: 9995 address: - ip: + ip: 127.0.0.1 logpath: /var/log/xxl-job/dw/ logretentiondays: 30 aop-log-record: diff --git a/servers/boe-server-all/src/main/resources/application-prod.yml b/servers/boe-server-all/src/main/resources/application-prod.yml index a9c7567f..34d42be1 100644 --- a/servers/boe-server-all/src/main/resources/application-prod.yml +++ b/servers/boe-server-all/src/main/resources/application-prod.yml @@ -146,6 +146,7 @@ boe: domain-name: https://u.boe.com pcPageUrl: ${boe.domain-name}/pc/course/studyindex?id= h5PageUrl: ${boe.domain-name}/mobile/pages/study/courseStudy?id= +# 问题所在:访问不了------- 页面闪退+502+404 pcLoginUrl: ${boe.domain-name}/web/ h5LoginUrl: ${boe.domain-name}/m/loginuser