Merge pull request 'AI generated changes for conversation 42' (#3) from xsha/lroyia/task-20251031-085600 into master

Reviewed-on: #3
This commit is contained in:
黎润豪 2025-10-31 17:07:53 +08:00
commit 6503e8a5d9
3 changed files with 521 additions and 0 deletions

View File

@ -19,9 +19,17 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.chinaweal.youfool.framework.springboot.rest.RestResult; import com.chinaweal.youfool.framework.springboot.rest.RestResult;
import com.chinaweal.youfool.framework.springboot.rest.BaseResultCode; 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;
/** /**
* 页面控制器 * 页面控制器
@ -223,6 +231,152 @@ public class PageController {
} }
} }
/**
* 发布课程页面
*
* @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;
}
}
/** /**
* 流式播放课程视频 * 流式播放课程视频
* *

View File

@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发布课程 - 课程管理系统</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.header {
background-color: #007bff;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.back-btn, .logout-btn {
background-color: #6c757d;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
transition: background-color 0.3s;
}
.back-btn:hover {
background-color: #5a6268;
}
.logout-btn:hover {
background-color: #c82333;
}
.content {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.form-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 2rem;
}
.form-title {
font-size: 1.5rem;
font-weight: bold;
color: #333;
margin-bottom: 1.5rem;
text-align: center;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
font-weight: bold;
color: #333;
margin-bottom: 0.5rem;
}
.form-label .required {
color: #dc3545;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
}
textarea.form-control {
min-height: 120px;
resize: vertical;
}
.file-input-group {
position: relative;
}
.file-input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.file-info {
margin-top: 0.5rem;
font-size: 0.85rem;
color: #666;
}
.submit-btn {
background-color: #007bff;
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
width: 100%;
transition: background-color 0.3s;
}
.submit-btn:hover {
background-color: #0056b3;
}
.submit-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.alert {
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
display: none;
}
.alert-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-danger {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.loading {
display: none;
text-align: center;
margin-top: 1rem;
color: #666;
}
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
.content {
margin: 1rem auto;
}
.form-container {
padding: 1rem;
}
}
</style>
</head>
<body>
<header class="header">
<h1>课程管理系统</h1>
<div class="user-info">
<a href="/course/index" class="back-btn">返回首页</a>
<span>欢迎,<span id="username"></span></span>
<a href="/course/user/auth/logout" class="logout-btn">退出登录</a>
</div>
</header>
<main class="content">
<div class="form-container">
<h2 class="form-title">发布新课程</h2>
<div id="alert" class="alert"></div>
<form id="publishForm" enctype="multipart/form-data">
<div class="form-group">
<label class="form-label">
课程标题 <span class="required">*</span>
</label>
<input type="text"
id="courseName"
name="courseName"
class="form-control"
placeholder="请输入课程标题"
required>
</div>
<div class="form-group">
<label class="form-label">
课程描述 <span class="required">*</span>
</label>
<textarea id="courseDesc"
name="courseDesc"
class="form-control"
placeholder="请输入课程描述,详细介绍课程内容、目标等"
required></textarea>
</div>
<div class="form-group">
<label class="form-label">
课程视频 <small>(可选,支持 mp4、avi、mov 等格式)</small>
</label>
<div class="file-input-group">
<input type="file"
id="videoFile"
name="videoFile"
class="file-input"
accept="video/*">
<div class="file-info">
请选择视频文件文件大小不超过100MB
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">
课程附件 <small>(可选,支持 pdf、doc、ppt 等格式)</small>
</label>
<div class="file-input-group">
<input type="file"
id="attachment"
name="attachment"
class="file-input"
accept=".pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.txt">
<div class="file-info">
请选择课程附件文件大小不超过100MB
</div>
</div>
</div>
<div class="form-group">
<button type="submit" id="submitBtn" class="submit-btn">
发布课程
</button>
</div>
<div id="loading" class="loading">
正在发布课程,请稍候...
</div>
</form>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 获取用户信息
async function loadUserInfo() {
try {
const response = await axios.get('/course/user/auth/login/info');
if (response.data.code === 200 && response.data.data) {
document.getElementById('username').textContent = response.data.data.username || '用户';
}
} catch (error) {
console.error('获取用户信息失败:', error);
window.location.href = '/course/login';
}
}
// 显示提示信息
function showAlert(message, type) {
const alert = document.getElementById('alert');
alert.className = `alert alert-${type}`;
alert.textContent = message;
alert.style.display = 'block';
// 3秒后自动隐藏
setTimeout(() => {
alert.style.display = 'none';
}, 3000);
}
// 显示/隐藏加载状态
function setLoading(loading) {
const submitBtn = document.getElementById('submitBtn');
const loadingDiv = document.getElementById('loading');
if (loading) {
submitBtn.disabled = true;
submitBtn.textContent = '发布中...';
loadingDiv.style.display = 'block';
} else {
submitBtn.disabled = false;
submitBtn.textContent = '发布课程';
loadingDiv.style.display = 'none';
}
}
// 发布课程
async function publishCourse(event) {
event.preventDefault();
const formData = new FormData();
formData.append('courseName', document.getElementById('courseName').value);
formData.append('courseDesc', document.getElementById('courseDesc').value);
const videoFile = document.getElementById('videoFile').files[0];
if (videoFile) {
formData.append('videoFile', videoFile);
}
const attachment = document.getElementById('attachment').files[0];
if (attachment) {
formData.append('attachment', attachment);
}
setLoading(true);
try {
const response = await axios.post('/course/publish', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
if (response.data.code === 200) {
showAlert('课程发布成功!', 'success');
// 2秒后跳转到首页
setTimeout(() => {
window.location.href = '/course/index';
}, 2000);
} else {
showAlert(response.data.message || '课程发布失败', 'danger');
}
} catch (error) {
console.error('发布课程失败:', error);
const errorMessage = error.response?.data?.message || error.message || '课程发布失败,请稍后重试';
showAlert(errorMessage, 'danger');
} finally {
setLoading(false);
}
}
// 文件选择时显示文件信息
function handleFileSelect(input, infoText) {
const file = input.files[0];
if (file) {
const fileSize = (file.size / 1024 / 1024).toFixed(2);
infoText.textContent = `已选择: ${file.name} (${fileSize}MB)`;
} else {
infoText.textContent = '';
}
}
// 页面加载时初始化
window.addEventListener('load', () => {
loadUserInfo();
// 绑定表单提交事件
document.getElementById('publishForm').addEventListener('submit', publishCourse);
// 绑定文件选择事件
document.getElementById('videoFile').addEventListener('change', function() {
const infoDiv = this.parentElement.querySelector('.file-info');
handleFileSelect(this, infoDiv);
});
document.getElementById('attachment').addEventListener('change', function() {
const infoDiv = this.parentElement.querySelector('.file-info');
handleFileSelect(this, infoDiv);
});
});
</script>
</body>
</html>

View File

@ -211,6 +211,7 @@
<header class="header"> <header class="header">
<h1>课程管理系统</h1> <h1>课程管理系统</h1>
<div class="user-info"> <div class="user-info">
<a href="/course/publish" class="logout-btn" style="background-color: #28a745;">发布课程</a>
<span>欢迎,<span id="username"></span></span> <span>欢迎,<span id="username"></span></span>
<a href="/course/user/auth/logout" class="logout-btn">退出登录</a> <a href="/course/user/auth/logout" class="logout-btn">退出登录</a>
</div> </div>