package com.chinaweal.youfool.course.controller; import cn.dev33.satoken.stp.StpUtil; import com.baomidou.mybatisplus.core.metadata.IPage; import com.chinaweal.youfool.course.entity.Course; import com.chinaweal.youfool.course.entity.CourseVideo; import com.chinaweal.youfool.course.entity.CourseAttachment; import com.chinaweal.youfool.course.entity.CourseComment; import com.chinaweal.youfool.course.service.CourseService; import com.chinaweal.youfool.course.service.CourseVideoService; import com.chinaweal.youfool.course.service.CourseAttachmentService; import com.chinaweal.youfool.course.service.CourseCommentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.chinaweal.youfool.framework.springboot.rest.RestResult; import com.chinaweal.youfool.framework.springboot.rest.BaseResultCode; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import java.io.File; import java.time.LocalDate; import java.time.LocalDateTime; import java.nio.file.Paths; import java.nio.file.Path; import java.util.UUID; /** * 页面控制器 * * @author lroyia * @since 2025/10/24 **/ @Controller public class PageController { @Autowired private CourseService courseService; @Autowired private CourseVideoService courseVideoService; @Autowired private CourseAttachmentService courseAttachmentService; @Autowired private CourseCommentService courseCommentService; /** * 登录页面 * * @return 登录页面 */ @GetMapping("/login") public String loginPage() { return "login"; } /** * 首页(带课程列表分页) * * @param pageNum 页码 * @param pageSize 每页大小 * @param courseName 课程名称搜索 * @param model 模型对象 * @return 首页 */ @GetMapping("/") public String index(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String courseName, Model model) { if (!StpUtil.isLogin()) { return "redirect:/course/login"; } // 获取课程分页数据 IPage coursePage = courseService.getCoursePage(pageNum, pageSize, courseName, null); // 添加数据到模型 model.addAttribute("coursePage", coursePage); model.addAttribute("courseName", courseName); model.addAttribute("pageNum", pageNum); model.addAttribute("pageSize", pageSize); return "index"; } /** * 首页(带课程列表分页) * * @param pageNum 页码 * @param pageSize 每页大小 * @param courseName 课程名称搜索 * @param model 模型对象 * @return 首页 */ @GetMapping("/index") public String indexPage(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String courseName, Model model) { if (!StpUtil.isLogin()) { return "redirect:/course/login"; } // 获取课程分页数据 IPage coursePage = courseService.getCoursePage(pageNum, pageSize, courseName, null); // 添加数据到模型 model.addAttribute("coursePage", coursePage); model.addAttribute("courseName", courseName); model.addAttribute("pageNum", pageNum); model.addAttribute("pageSize", pageSize); return "index"; } /** * 课程详情页面 * * @param id 课程ID * @param commentPage 评论页码 * @param model 模型对象 * @return 课程详情页面 */ @GetMapping("/detail/{id}") public String courseDetail(@PathVariable String id, @RequestParam(defaultValue = "1") Integer commentPage, Model model) { if (!StpUtil.isLogin()) { return "redirect:/course/login"; } // 获取课程基本信息 Course course = courseService.getById(id); if (course == null) { return "redirect:/course/index"; } // 获取课程视频信息 CourseVideo courseVideo = courseVideoService.getCourseMainVideo(id); // 获取课程附件列表 java.util.List attachments = courseAttachmentService.getAttachmentsByCourseId(id); // 获取评论分页数据 IPage comments = courseCommentService.getCommentsByCourseId(id, commentPage, 10); // 添加数据到模型 model.addAttribute("course", course); model.addAttribute("courseVideo", courseVideo); model.addAttribute("attachments", attachments); model.addAttribute("comments", comments); model.addAttribute("commentPage", commentPage); return "course-detail"; } /** * 添加课程评论 * * @param commentData 评论数据 * @return 添加结果 */ @PostMapping("/comment/add") @ResponseBody public RestResult addComment(@RequestBody java.util.Map commentData) { if (!StpUtil.isLogin()) { return RestResult.error(BaseResultCode.BUSINESS_LOGIC_ERROR, "请先登录"); } try { String courseId = (String) commentData.get("courseId"); String commentText = (String) commentData.get("commentText"); String parentId = (String) commentData.get("parentId"); CourseComment comment = new CourseComment(); comment.setCourseId(courseId); comment.setCommentText(commentText); comment.setParentId(parentId); comment.setCreateBy(StpUtil.getLoginIdAsString()); boolean success = courseCommentService.save(comment); return success ? RestResult.ok(true) : RestResult.error(BaseResultCode.BUSINESS_LOGIC_ERROR, "评论发表失败"); } catch (Exception e) { return RestResult.error(BaseResultCode.BUSINESS_LOGIC_ERROR, "评论发表失败:" + e.getMessage()); } } /** * 下载课程附件 * * @param attachmentId 附件ID * @return 附件文件 */ @GetMapping("/attachment/download/{attachmentId}") public org.springframework.http.ResponseEntity downloadAttachment(@PathVariable String attachmentId) { if (!StpUtil.isLogin()) { return org.springframework.http.ResponseEntity.status(org.springframework.http.HttpStatus.FOUND) .location(ServletUriComponentsBuilder.fromPath("/course/login").build().toUri()) .build(); } CourseAttachment attachment = courseAttachmentService.getById(attachmentId); if (attachment == null) { return org.springframework.http.ResponseEntity.notFound().build(); } try { java.io.File file = new java.io.File(attachment.getAbsoluteName()); if (!file.exists()) { return org.springframework.http.ResponseEntity.notFound().build(); } org.springframework.core.io.Resource resource = new org.springframework.core.io.UrlResource(file.toURI()); return org.springframework.http.ResponseEntity.ok() .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + new String(attachment.getFileName().getBytes("UTF-8"), "ISO-8859-1") + "\"") .header(org.springframework.http.HttpHeaders.CONTENT_TYPE, "application/octet-stream") .body(resource); } catch (Exception e) { return org.springframework.http.ResponseEntity.status(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR).build(); } } /** * 发布课程页面 * * @return 发布课程页面 */ @GetMapping("/publish") public String publishCoursePage(Model model) { if (!StpUtil.isLogin()) { return "redirect:/course/login"; } return "course-publish"; } /** * 发布课程 * * @param courseName 课程名称 * @param courseDesc 课程描述 * @param videoFile 课程视频文件(可选) * @param attachment 课程附件文件(可选) * @param model 模型对象 * @return 发布结果 */ @PostMapping("/publish") @DSTransactional @ResponseBody public RestResult publishCourse( @RequestParam String courseName, @RequestParam String courseDesc, @RequestParam(required = false) MultipartFile videoFile, @RequestParam(required = false) MultipartFile attachment, Model model) { if (!StpUtil.isLogin()) { return RestResult.error(BaseResultCode.BUSINESS_LOGIC_ERROR, "请先登录"); } if (courseName == null || courseName.trim().isEmpty()) { return RestResult.error(BaseResultCode.BUSINESS_LOGIC_ERROR, "课程标题不能为空"); } if (courseDesc == null || courseDesc.trim().isEmpty()) { return RestResult.error(BaseResultCode.BUSINESS_LOGIC_ERROR, "课程描述不能为空"); } try { // 创建课程 Course course = new Course(); course.setCourseName(courseName.trim()); course.setCourseDesc(courseDesc.trim()); course.setCourseDate(LocalDate.now()); course.setCreateBy(StpUtil.getLoginIdAsString()); course.setCreateTime(LocalDateTime.now()); boolean courseSaved = courseService.save(course); if (!courseSaved) { return RestResult.error(BaseResultCode.BUSINESS_LOGIC_ERROR, "创建课程失败"); } String courseId = course.getId(); // 处理视频文件 if (videoFile != null && !videoFile.isEmpty()) { String videoAttachmentId = saveUploadFile(videoFile, courseId, "video"); if (videoAttachmentId != null) { CourseVideo courseVideo = new CourseVideo(); courseVideo.setCourseId(courseId); courseVideo.setSortIdx(1); courseVideo.setVideoName(videoFile.getOriginalFilename()); courseVideo.setAttachmentId(videoAttachmentId); courseVideo.setCreateBy(StpUtil.getLoginIdAsString()); courseVideo.setCreateTime(LocalDateTime.now()); courseVideoService.save(courseVideo); } } // 处理附件文件 if (attachment != null && !attachment.isEmpty()) { saveUploadFile(attachment, courseId, "attachment"); } return RestResult.ok(courseId, "课程发布成功"); } catch (Exception e) { e.printStackTrace(); return RestResult.error(BaseResultCode.BUSINESS_LOGIC_ERROR, "课程发布失败:" + e.getMessage()); } } /** * 保存上传的文件 * * @param file 上传的文件 * @param courseId 课程ID * @param type 文件类型(video或attachment) * @return 附件ID */ private String saveUploadFile(MultipartFile file, String courseId, String type) { try { if (file.isEmpty()) { return null; } String originalFilename = file.getOriginalFilename(); if (originalFilename == null) { return null; } String fileFormat = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase(); String newFileName = UUID.randomUUID().toString() + "." + fileFormat; // 创建上传目录 String uploadDir = "../uploads/courses/" + LocalDate.now().getYear() + "/" + String.format("%02d", LocalDate.now().getMonthValue()) + "/" + String.format("%02d", LocalDate.now().getDayOfMonth()); Path uploadPath = Paths.get(uploadDir); if (!uploadPath.toFile().exists()) { uploadPath.toFile().mkdirs(); } String absolutePath = uploadDir + "/" + newFileName; // 保存文件 File dest = new File(absolutePath); file.transferTo(dest); // 创建附件记录 CourseAttachment attachment = new CourseAttachment(); attachment.setCourseId(courseId); attachment.setFileName(originalFilename); attachment.setFileFormat(fileFormat); attachment.setAbsoluteName(absolutePath); attachment.setFileSize(file.getSize()); attachment.setCreateBy(StpUtil.getLoginIdAsString()); attachment.setCreateTime(LocalDateTime.now()); boolean saved = courseAttachmentService.save(attachment); return saved ? attachment.getId() : null; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 流式播放课程视频 * * @param attachmentId 附件ID * @return 视频文件流 */ @GetMapping("/course/video/stream/{attachmentId}") public org.springframework.http.ResponseEntity streamVideo(@PathVariable String attachmentId) { if (!StpUtil.isLogin()) { return org.springframework.http.ResponseEntity.status(org.springframework.http.HttpStatus.FOUND) .location(ServletUriComponentsBuilder.fromPath("/course/login").build().toUri()) .build(); } CourseAttachment attachment = courseAttachmentService.getById(attachmentId); if (attachment == null) { return org.springframework.http.ResponseEntity.notFound().build(); } try { java.io.File file = new java.io.File(attachment.getAbsoluteName()); if (!file.exists()) { return org.springframework.http.ResponseEntity.notFound().build(); } org.springframework.core.io.Resource resource = new org.springframework.core.io.UrlResource(file.toURI()); String contentType = determineVideoContentType(attachment.getFileFormat()); return org.springframework.http.ResponseEntity.ok() .header(org.springframework.http.HttpHeaders.CONTENT_TYPE, contentType) .header(org.springframework.http.HttpHeaders.ACCEPT_RANGES, "bytes") .body(resource); } catch (Exception e) { return org.springframework.http.ResponseEntity.status(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR).build(); } } /** * 根据文件格式确定视频内容类型 * * @param fileFormat 文件格式 * @return MIME类型 */ private String determineVideoContentType(String fileFormat) { if (fileFormat == null) { return "video/mp4"; } switch (fileFormat.toLowerCase()) { case "mp4": return "video/mp4"; case "webm": return "video/webm"; case "ogg": return "video/ogg"; case "avi": return "video/x-msvideo"; case "mov": return "video/quicktime"; case "wmv": return "video/x-ms-wmv"; case "flv": return "video/x-flv"; default: return "video/mp4"; } } }