feat: 🎸 课程报名、导入学生、上传视频切片接口&dockerfile增加ffmpeg

This commit is contained in:
GoCo 2025-08-20 05:53:27 +08:00
parent 7188b7a500
commit 8717b32306
8 changed files with 486 additions and 68 deletions

View File

@ -13,10 +13,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.biz.dto.CourseWithTeacherInfo;
import org.jeecg.modules.biz.dto.TeacherInfo;
@ -28,6 +31,7 @@ import org.jeecg.modules.gen.homework.entity.Homework;
import org.jeecg.modules.gen.resource.entity.Resource;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
@ -162,6 +166,34 @@ public class CourseBizController {
return Result.OK(isEnrolled);
}
@GetMapping("/teacher_list")
@Operation(summary = "查询当前教师创建的课程")
public Result<List<Course>> queryTeacherCourseList(HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseApi.getUserByName(username);
System.out.println(sysUser.getId());
List<Course> list = courseBizService.list(new QueryWrapper<Course>().eq("create_by", sysUser.getUsername()));
return Result.OK(list);
}
@PostMapping("/{courseId}/add_students")
@Operation(summary = "批量导入学生", description = "请求体为JSON格式包含ids字段ids为逗号分隔的学生ID字符串")
public Result<Map<String, Object>> addStudents(@PathVariable(value = "courseId") String courseId, @RequestBody Map<String, Object> requestBody, HttpServletRequest request) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseApi.getUserByName(username);
// 从Map中获取ids字段
String ids = (String) requestBody.get("ids");
if (ids == null || ids.trim().isEmpty()) {
return Result.error("ids字段不能为空");
}
return Result.OK(courseBizService.addStudents(courseId, ids, sysUser));
}
@GetMapping("/test")
@IgnoreAuth
public Result<String> test() {

View File

@ -14,13 +14,11 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.biz.constant.EntityLinkConst;
import org.jeecg.modules.biz.service.EntityLinkBizService;
@ -29,7 +27,7 @@ import org.jeecg.modules.gen.resource.entity.Resource;
import io.swagger.v3.oas.annotations.Operation;
@Tag(name = "02-资源管理")
@Tag(name = "资源")
@RestController
@RequestMapping("/biz/resource")
@Slf4j
@ -80,11 +78,10 @@ public class ResourceBizController {
}
@PostMapping("/upload")
@Operation(summary = "课程视频文件上传", description = "课程视频文件上传返回m3u8文件地址")
@IgnoreAuth
public Result<String> upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws Exception {
@Operation(summary = "课程视频文件上传", description = "课程视频文件上传返回各清晰度的m3u8文件地址")
public Result<Map<String, String>> upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws Exception {
if (file == null || file.isEmpty()) return Result.error("没有找到上传的文件");
String url = resourceBizService.uploadHls(file, request);
return Result.OK(url);
Map<String, String> qualityUrls = resourceBizService.uploadHls(file, request);
return Result.OK(qualityUrls);
}
}

View File

@ -1,9 +1,11 @@
package org.jeecg.modules.biz.service;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.biz.dto.CourseWithTeacherInfo;
import org.jeecg.modules.biz.dto.TeacherInfo;
import org.jeecg.modules.gen.course.entity.Course;
@ -80,6 +82,15 @@ public interface CourseBizService extends IService<Course> {
* @return
*/
boolean isEnrolled(String courseId, String id);
/**
* 批量导入学生
* @param courseId
* @param ids
* @param sysUser
* @return
*/
Map<String, Object> addStudents(String courseId, String ids, LoginUser sysUser);
}

View File

@ -1,6 +1,7 @@
package org.jeecg.modules.biz.service;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import org.jeecg.modules.gen.resource.entity.Resource;
import org.springframework.web.multipart.MultipartFile;
@ -12,13 +13,13 @@ import com.baomidou.mybatisplus.extension.service.IService;
public interface ResourceBizService extends IService<Resource> {
/**
* 上传视频并切片为 HLSm3u8+ts按配置(local|minio|alioss)上传返回 m3u8 路径/URL
* 上传视频并切片为 HLSm3u8+ts按配置(local|minio|alioss)上传返回各清晰度的 m3u8 路径/URL
* @param file 上传的视频文件
* @param request 用于读取 header 或环境配置
* @return m3u8 路径/URL
* @return 各清晰度的 m3u8 路径/URLMap的key为清晰度名称"480p", "720p", "1080p"value为对应的URL
* @throws Exception 处理异常
*/
String uploadHls(MultipartFile file, HttpServletRequest request) throws Exception;
Map<String, String> uploadHls(MultipartFile file, HttpServletRequest request) throws Exception;
}

View File

@ -1,6 +1,8 @@
package org.jeecg.modules.biz.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.MinioUtil;
import org.jeecg.common.util.SpringContextUtils;
@ -27,6 +29,9 @@ import org.jeecg.modules.gen.userinfo.mapper.UserInfoMapper;
import org.jeecg.modules.system.entity.SysUser;
import org.jeecg.modules.system.mapper.SysUserMapper;
import org.springframework.beans.BeanUtils;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -80,6 +85,20 @@ public class CourseBizServiceImpl extends ServiceImpl<CourseMapper, Course> impl
@Autowired
private CourseSignupMapper courseSignupMapper;
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 将Map转换为JSON字符串
*/
private String toJsonString(Map<String, Object> map) {
try {
return objectMapper.writeValueAsString(map);
} catch (Exception e) {
log.error("JSON转换失败", e);
return map.toString();
}
}
@Override
public <T> List<T> getCourseSectionDetail(Integer type, String sectionId, Class<T> clazz) {
// 1. 查询章节是否存在
@ -382,7 +401,11 @@ public class CourseBizServiceImpl extends ServiceImpl<CourseMapper, Course> impl
signupQuery.eq("user_id", userId).eq("course_id", courseId);
CourseSignup existingSignup = courseSignupMapper.selectOne(signupQuery);
if (existingSignup != null) {
return "success"; // 已报名
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("code", "already_enrolled");
result.put("message", "用户已经报名该课程");
return toJsonString(result);
}
// 4. 检查课程是否可以报名
@ -414,7 +437,11 @@ public class CourseBizServiceImpl extends ServiceImpl<CourseMapper, Course> impl
course.setEnrollCount(currentEnrollCount + 1);
courseMapper.updateById(course);
return "success";
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("code", "success");
result.put("message", "报名成功");
return toJsonString(result);
}
@Override
@ -424,6 +451,121 @@ public class CourseBizServiceImpl extends ServiceImpl<CourseMapper, Course> impl
CourseSignup existingSignup = courseSignupMapper.selectOne(signupQuery);
return existingSignup != null;
}
@Override
@Transactional
public Map<String, Object> addStudents(String courseId, String ids, LoginUser teacher) {
// 1. 参数校验
if (courseId == null || courseId.trim().isEmpty()) {
throw new RuntimeException("课程ID不能为空");
}
if (ids == null || ids.isEmpty()) {
throw new RuntimeException("学生ID列表不能为空");
}
if (teacher == null || teacher.getId() == null) {
throw new RuntimeException("教师信息不能为空");
}
// 2. 查询课程是否存在
Course course = courseMapper.selectById(courseId);
if (course == null) {
throw new RuntimeException("课程不存在");
}
// 3. 权限校验 - 检查当前登录用户是否是课程的创建人
if (!teacher.getUsername().equals(course.getCreateBy())) {
throw new RuntimeException("只有课程创建者才能添加学生");
}
// 4. 过滤有效的学生ID
List<String> validStudentIds = new ArrayList<>();
for (String studentId : ids.split(",")) {
if (studentId != null && !studentId.trim().isEmpty()) {
validStudentIds.add(studentId.trim());
}
}
if (validStudentIds.isEmpty()) {
throw new RuntimeException("没有有效的学生ID");
}
// 5. 检查哪些学生已经报名
QueryWrapper<CourseSignup> existingQuery = new QueryWrapper<>();
existingQuery.eq("course_id", courseId).in("user_id", validStudentIds);
List<CourseSignup> existingSignups = courseSignupMapper.selectList(existingQuery);
Set<String> alreadyEnrolledIds = new HashSet<>();
for (CourseSignup signup : existingSignups) {
alreadyEnrolledIds.add(signup.getUserId());
}
// 6. 过滤出需要新增的学生ID
List<String> newStudentIds = new ArrayList<>();
for (String studentId : validStudentIds) {
if (!alreadyEnrolledIds.contains(studentId)) {
newStudentIds.add(studentId);
}
}
if (newStudentIds.isEmpty()) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("code", "all_already_enrolled");
result.put("message", "所有学生都已经报名");
result.put("addedCount", 0);
result.put("alreadyEnrolledCount", alreadyEnrolledIds.size());
return result;
}
// 7. 检查报名人数限制
int currentEnrollCount = course.getEnrollCount() == null ? 0 : course.getEnrollCount();
if (course.getMaxEnroll() != null) {
if (currentEnrollCount + newStudentIds.size() > course.getMaxEnroll()) {
throw new RuntimeException("添加学生后将超过最大报名人数限制,当前:" + currentEnrollCount + ",最大:" + course.getMaxEnroll() + ",新增:" + newStudentIds.size());
}
}
// 8. 批量创建报名记录
List<CourseSignup> newSignups = new ArrayList<>();
Date now = new Date();
for (String studentId : newStudentIds) {
CourseSignup signup = new CourseSignup();
signup.setUserId(studentId);
signup.setCourseId(courseId);
signup.setCreateTime(now);
signup.setCreateBy(teacher.getId());
newSignups.add(signup);
}
// 9. 批量插入报名记录
for (CourseSignup signup : newSignups) {
int insertResult = courseSignupMapper.insert(signup);
if (insertResult <= 0) {
throw new RuntimeException("插入报名记录失败学生ID: " + signup.getUserId());
}
}
// 10. 更新课程报名人数
course.setEnrollCount(currentEnrollCount + newStudentIds.size());
courseMapper.updateById(course);
// 11. 返回结果
Map<String, Object> result = new HashMap<>();
if (alreadyEnrolledIds.isEmpty()) {
result.put("success", true);
result.put("code", "success");
result.put("message", "所有学生都成功添加");
result.put("addedCount", newStudentIds.size());
result.put("alreadyEnrolledCount", 0);
} else {
result.put("success", true);
result.put("code", "partial_success");
result.put("message", "部分学生添加成功,部分学生已经报名");
result.put("addedCount", newStudentIds.size());
result.put("alreadyEnrolledCount", alreadyEnrolledIds.size());
}
return result;
}
}

View File

@ -1,12 +1,13 @@
package org.jeecg.modules.biz.service.impl;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.MinioUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oss.OssBootUtil;
import org.jeecg.modules.biz.service.ResourceBizService;
import org.jeecg.modules.gen.test.mapper.TestTableMapper;
import org.jeecg.modules.gen.resource.mapper.ResourceMapper;
import org.jeecg.modules.gen.resource.entity.Resource;
import org.springframework.stereotype.Service;
@ -23,81 +24,287 @@ import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@Slf4j
@Service
public class ResourceBizServiceImpl extends ServiceImpl<ResourceMapper, Resource> implements ResourceBizService {
/**
* 视频清晰度配置
*/
@Data
@AllArgsConstructor
public static class VideoQuality {
private String name; // 清晰度名称
private String outputDir; // 输出目录
private int width; // 宽度
private int height; // 高度
private String bitrate; // 视频比特率
private String audioRate; // 音频比特率
}
// 预定义的清晰度配置
private static final List<VideoQuality> QUALITY_CONFIGS = Arrays.asList(
new VideoQuality("480p", "480p", 854, 480, "1000k", "96k"),
new VideoQuality("720p", "720p", 1280, 720, "2500k", "128k"),
new VideoQuality("1080p", "1080p", 1920, 1080, "5000k", "192k")
);
private static final int SEGMENT_TIME = 10; // HLS切片时长
private static final ExecutorService executorService = Executors.newFixedThreadPool(3);
@Override
public String uploadHls(MultipartFile file, HttpServletRequest request) throws Exception {
// 读取上传类型header 优先
String headerUploadType = request.getHeader("uploadType");
public Map<String, String> uploadHls(MultipartFile file, HttpServletRequest request) throws Exception {
// 读取上传类型
String configUploadType = SpringContextUtils.getApplicationContext().getEnvironment().getProperty("jeecg.uploadType", "minio");
String uploadType = (headerUploadType != null && headerUploadType.trim().length() > 0) ? headerUploadType : configUploadType;
String uploadType = configUploadType;
// 1) 保存临时原始视频
String uuid = UUID.randomUUID().toString();
String tmpRoot = System.getProperty("java.io.tmpdir");
Path tmpVideoDir = Path.of(tmpRoot, "jeecg", "video", uuid);
Path hlsDir = Path.of(tmpRoot, "jeecg", "hls", uuid);
Path hlsBaseDir = Path.of(tmpRoot, "jeecg", "hls", uuid);
Files.createDirectories(tmpVideoDir);
Files.createDirectories(hlsDir);
Files.createDirectories(hlsBaseDir);
String original = CommonUtils.getFileName(Objects.requireNonNull(file.getOriginalFilename()));
Path tmpVideoFile = tmpVideoDir.resolve(original);
Files.copy(file.getInputStream(), tmpVideoFile, StandardCopyOption.REPLACE_EXISTING);
// 2) ffmpeg 切片
Path m3u8Path = hlsDir.resolve(uuid + ".m3u8");
List<String> cmd = Arrays.asList(
"ffmpeg", "-i", tmpVideoFile.toString(),
"-c:v", "libx264", "-c:a", "aac",
"-hls_time", "10", "-hls_playlist_type", "vod",
m3u8Path.toString());
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
boolean ok = p.waitFor(10, TimeUnit.MINUTES) && p.exitValue() == 0;
if (!ok) {
deleteQuietly(hlsDir.toFile());
deleteQuietly(tmpVideoDir.toFile());
throw new RuntimeException("ffmpeg切片超时");
}
log.info("开始多清晰度视频处理,文件: {}", original);
// 3) 上传切片
try {
// 2) 并行处理多个清晰度
List<CompletableFuture<Map<String, String>>> futures = new ArrayList<>();
for (VideoQuality quality : QUALITY_CONFIGS) {
CompletableFuture<Map<String, String>> future = CompletableFuture.supplyAsync(() -> {
try {
return processVideoQuality(tmpVideoFile, hlsBaseDir, quality, uploadType, uuid);
} catch (Exception e) {
log.error("处理{}清晰度失败", quality.getName(), e);
throw new RuntimeException("处理" + quality.getName() + "清晰度失败: " + e.getMessage());
}
}, executorService);
futures.add(future);
}
// 3) 等待所有清晰度处理完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 设置超时时间为30分钟
allFutures.get(30, TimeUnit.MINUTES);
// 4) 收集结果 - 各清晰度的index.m3u8地址
Map<String, String> qualityUrls = new HashMap<>();
for (CompletableFuture<Map<String, String>> future : futures) {
qualityUrls.putAll(future.get());
}
log.info("多清晰度视频处理完成,各清晰度地址: {}", qualityUrls);
return qualityUrls;
} finally {
// 清理临时文件
deleteQuietly(hlsBaseDir.toFile());
deleteQuietly(tmpVideoDir.toFile());
}
}
/**
* 处理单个清晰度的视频切片
*/
private Map<String, String> processVideoQuality(Path inputFile, Path hlsBaseDir, VideoQuality quality, String uploadType, String uuid) throws Exception {
log.info("开始处理 {} 清晰度...", quality.getName());
// 创建清晰度输出目录
Path outputDir = hlsBaseDir.resolve(quality.getOutputDir());
Files.createDirectories(outputDir);
Path playlistPath = outputDir.resolve("index.m3u8");
// 构建FFmpeg命令 - 参考Go代码
List<String> args = Arrays.asList(
"ffmpeg", "-i", inputFile.toString(),
"-vf", String.format("scale=%d:%d", quality.getWidth(), quality.getHeight()),
"-c:v", "libx264",
"-b:v", quality.getBitrate(),
"-c:a", "aac",
"-b:a", quality.getAudioRate(),
"-hls_time", String.valueOf(SEGMENT_TIME),
"-hls_playlist_type", "vod",
"-hls_segment_filename", outputDir.resolve("segment%d.ts").toString(),
"-y", // 覆盖已存在的文件
playlistPath.toString()
);
// 执行FFmpeg并处理日志
executeFFmpegWithLogging(args, quality.getName());
log.info("{} 清晰度处理完成", quality.getName());
// 上传切片文件
return uploadQualityFiles(outputDir, quality, uploadType, uuid);
}
/**
* 上传单个清晰度的所有文件
*/
private Map<String, String> uploadQualityFiles(Path outputDir, VideoQuality quality, String uploadType, String uuid) throws Exception {
Map<String, String> urls = new HashMap<>();
String basePrefix = "video/hls/" + uuid + "/" + quality.getOutputDir();
String m3u8Url = "";
String base = "video/hls/" + uuid;
try (Stream<Path> paths = Files.list(hlsDir)) {
for (Path f : (Iterable<Path>) paths::iterator) {
if (!Files.isRegularFile(f)) continue;
String rel = base + "/" + f.getFileName().toString();
try (InputStream in = Files.newInputStream(f)) {
try (Stream<Path> paths = Files.list(outputDir)) {
for (Path file : (Iterable<Path>) paths::iterator) {
if (!Files.isRegularFile(file)) continue;
String fileName = file.getFileName().toString();
String relativePath = basePrefix + "/" + fileName;
try (InputStream in = Files.newInputStream(file)) {
String fileUrl = "";
if ("minio".equals(uploadType)) {
String tmpUrl = MinioUtil.upload(in, rel);
if (f.getFileName().toString().endsWith(".m3u8")) {
m3u8Url = tmpUrl;
}
fileUrl = MinioUtil.upload(in, relativePath);
} else if ("alioss".equals(uploadType)) {
OssBootUtil.upload(in, rel);
if (f.getFileName().toString().endsWith(".m3u8")) {
m3u8Url = rel; // 可在网关拼域名
}
OssBootUtil.upload(in, relativePath);
fileUrl = relativePath; // 可在网关拼域名
} else {
// 本地存储
String uploadpath = SpringContextUtils.getApplicationContext().getEnvironment().getProperty("jeecg.path.upload");
Path target = Path.of(uploadpath, rel);
Path target = Path.of(uploadpath, relativePath);
Files.createDirectories(target.getParent());
Files.copy(f, target, StandardCopyOption.REPLACE_EXISTING);
if (f.getFileName().toString().endsWith(".m3u8")) {
m3u8Url = rel; // local 返回相对路径
}
Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING);
fileUrl = relativePath;
}
if (fileName.endsWith(".m3u8")) {
m3u8Url = fileUrl;
}
}
}
} finally {
deleteQuietly(hlsDir.toFile());
deleteQuietly(tmpVideoDir.toFile());
}
return m3u8Url;
urls.put(quality.getName(), m3u8Url);
return urls;
}
/**
* 执行FFmpeg命令并处理日志输出
*/
private void executeFFmpegWithLogging(List<String> args, String qualityName) throws Exception {
// 打印FFmpeg命令
log.info("执行FFmpeg命令[{}]: {}", qualityName, String.join(" ", args));
ProcessBuilder pb = new ProcessBuilder(args);
pb.redirectErrorStream(true); // 将错误输出重定向到标准输出
Process process = pb.start();
// 使用单独的线程来读取FFmpeg输出避免阻塞
StringBuilder ffmpegOutput = new StringBuilder();
Thread logThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
synchronized (ffmpegOutput) {
ffmpegOutput.append(line).append("\n");
}
// 输出详细日志到DEBUG级别
log.debug("FFmpeg[{}]: {}", qualityName, line);
// 过滤并输出关键进度信息
if (line.contains("frame=") && line.contains("time=")) {
// 提取进度信息
String progressInfo = extractProgressInfo(line);
if (progressInfo != null) {
log.info("FFmpeg进度[{}]: {}", qualityName, progressInfo);
}
}
// 输出警告和错误信息
if (line.toLowerCase().contains("warning") || line.toLowerCase().contains("error")) {
log.warn("FFmpeg[{}]: {}", qualityName, line);
}
// 输出关键状态信息
if (line.contains("Stream mapping:") || line.contains("Press [q] to stop") ||
line.contains("video:") || line.contains("audio:")) {
log.info("FFmpeg[{}]: {}", qualityName, line);
}
}
} catch (Exception e) {
log.error("读取FFmpeg输出时发生错误[{}]", qualityName, e);
}
});
logThread.setDaemon(true);
logThread.start();
// 等待进程完成
boolean finished = process.waitFor(15, TimeUnit.MINUTES);
int exitCode = process.exitValue();
// 等待日志线程完成
logThread.join(5000); // 最多等待5秒
if (!finished || exitCode != 0) {
synchronized (ffmpegOutput) {
log.error("FFmpeg处理[{}]失败,退出码: {}", qualityName, exitCode);
log.error("FFmpeg完整输出[{}]:\n{}", qualityName, ffmpegOutput.toString());
}
throw new RuntimeException(String.format("FFmpeg处理%s失败退出码: %d", qualityName, exitCode));
}
log.info("FFmpeg处理[{}]成功完成", qualityName);
}
/**
* 从FFmpeg输出行中提取进度信息
*/
private String extractProgressInfo(String line) {
try {
// FFmpeg进度行格式示例
// frame= 1234 fps= 25 q=23.0 size= 12345kB time=00:01:23.45 bitrate=1234.5kbits/s speed=1.23x
if (line.contains("time=") && line.contains("bitrate=")) {
// 提取时间和比特率信息
String time = "";
String bitrate = "";
String speed = "";
String frame = "";
String[] parts = line.split("\\s+");
for (String part : parts) {
if (part.startsWith("time=")) {
time = part.substring(5);
} else if (part.startsWith("bitrate=")) {
bitrate = part.substring(8);
} else if (part.startsWith("speed=")) {
speed = part.substring(6);
} else if (part.startsWith("frame=")) {
frame = part.substring(6);
}
}
if (!time.isEmpty()) {
return String.format("frame=%s time=%s bitrate=%s speed=%s", frame, time, bitrate, speed);
}
}
} catch (Exception e) {
// 解析失败时忽略返回原始行
return line.trim();
}
return null;
}
/** 删除临时目录文件 */
private static void deleteQuietly(File file) {

View File

@ -44,7 +44,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
* @Date: 2025-08-09
* @Version: V1.0
*/
@Tag(name="课程")
@Tag(name="课程")
@RestController
@RequestMapping("/gen/course/course")
@Slf4j
@ -82,11 +82,16 @@ public class CourseController extends JeecgController<Course, ICourseService> {
* @param course
* @return
*/
@AutoLog(value = "课程-添加")
@Operation(summary="课程-添加")
@AutoLog(value = "课程-添加")
@Operation(summary="课程-添加")
@RequiresPermissions("gen.course:course:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody Course course) {
// 判断开始时间是否晚于结束时间
if (course.getStartTime().after(course.getEndTime())) {
return Result.error("开始时间不能晚于结束时间");
}
courseService.save(course);
return Result.OK("添加成功!");
@ -98,11 +103,17 @@ public class CourseController extends JeecgController<Course, ICourseService> {
* @param course
* @return
*/
@AutoLog(value = "课程-编辑")
@Operation(summary="课程-编辑")
@AutoLog(value = "课程-编辑")
@Operation(summary="课程-编辑")
@RequiresPermissions("gen.course:course:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody Course course) {
// 判断开始时间是否晚于结束时间
if (course.getStartTime().after(course.getEndTime())) {
return Result.error("开始时间不能晚于结束时间");
}
courseService.updateById(course);
return Result.OK("编辑成功!");
}
@ -113,8 +124,8 @@ public class CourseController extends JeecgController<Course, ICourseService> {
* @param id
* @return
*/
@AutoLog(value = "课程-通过id删除")
@Operation(summary="课程-通过id删除")
@AutoLog(value = "课程-通过id删除")
@Operation(summary="课程-通过id删除")
@RequiresPermissions("gen.course:course:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name="id",required=true) String id) {

View File

@ -2,15 +2,32 @@ FROM registry.cn-hangzhou.aliyuncs.com/dockerhub_mirror/java:17-anolis
MAINTAINER jeecgos@163.com
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 安装FFmpeg和设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
# 安装curl如果没有的话
yum install -y curl xz && \
# 下载并安装FFmpeg静态编译版本
curl -L https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz | tar -xJ -C /tmp && \
mv /tmp/ffmpeg-*-amd64-static/ffmpeg /usr/local/bin/ && \
mv /tmp/ffmpeg-*-amd64-static/ffprobe /usr/local/bin/ && \
chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe && \
# 清理临时文件
rm -rf /tmp/ffmpeg-* && \
yum clean all && \
rm -rf /var/cache/yum && \
# 验证FFmpeg安装
ffmpeg -version
# 创建应用目录
#RUN mkdir -p /jeecg-boot/config/jeecg/
WORKDIR /jeecg-boot
EXPOSE 8080
# 复制应用文件
#ADD ./src/main/resources/jeecg ./config/jeecg
ADD ./target/jeecg-system-start-3.8.2.jar ./
# 启动应用
CMD sleep 60;java -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -jar jeecg-system-start-3.8.2.jar