generated from youfool-project/youfool-prj-springboot-template
448 lines
16 KiB
Java
448 lines
16 KiB
Java
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<Course> 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<Course> 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<CourseAttachment> attachments = courseAttachmentService.getAttachmentsByCourseId(id);
|
||
|
||
// 获取评论分页数据
|
||
IPage<CourseComment> 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<Boolean> addComment(@RequestBody java.util.Map<String, Object> 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<org.springframework.core.io.Resource> 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<String> 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<org.springframework.core.io.Resource> 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";
|
||
}
|
||
}
|
||
} |