feat(chaos): 实现视频分片上传和后台处理功能- 新增视频上传相关控制器、服务接口和实现类

- 实现了视频分片上传、合并和后台处理的逻辑
- 添加了 RabbitMQ 消息队列配置和消息转换器
-优化了 JWT 认证过滤器和日志记录
- 新增了跨域配置
This commit is contained in:
Chaos
2025-07-20 07:17:30 +08:00
parent 287394e8f5
commit 3683a9d8e0
23 changed files with 916 additions and 7 deletions

View File

@@ -63,6 +63,11 @@
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
@@ -71,7 +76,7 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.0</version>
<executions>
<execution>
<goals>

View File

@@ -0,0 +1,27 @@
package cn.nopj.chaos_api.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 匹配所有路径
.allowedOrigins(
"http://localhost:63342", // 你的前端运行的地址
"http://localhost:8080", // 其他可能的前端地址
"http://127.0.0.1:5500", // 另一个常见的本地开发地址
"null"
) // 允许的来源
// 请根据你的前端实际部署地址修改或增加这里的源
// 在开发环境可以加 "null" 来处理从文件系统打开的页面
// 生产环境请务必替换为明确的域名或IP避免使用"*"或"null"
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法
.allowedHeaders("*") // 允许所有请求头
.allowCredentials(true) // 允许发送认证信息(如 Cookies, HTTP 认证及客户端 SSL 证书)
.maxAge(3600); // 预检请求的缓存时间(秒)
}
}

View File

@@ -1,10 +1,13 @@
package cn.nopj.chaos_api.config.sec;
package cn.nopj.chaos_api.config;
import cn.nopj.chaos_api.config.jwt.JwtAuthenticationTokenFilter;
import cn.nopj.chaos_api.config.sec.RestAuthenticationEntryPoint;
import cn.nopj.chaos_api.config.sec.RestfulAccessDeniedHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -13,9 +16,16 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@@ -51,9 +61,30 @@ public class SecurityConfig {
.exceptionHandling(e -> e
.authenticationEntryPoint(restAuthenticationEntryPoint)
.accessDeniedHandler(restfulAccessDeniedHandler))
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
;
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(
"http://localhost:63342", // 你的前端运行的地址
"http://localhost:8080", // 其他可能的前端地址
"http://127.0.0.1:5500", // 另一个常见的本地开发地址
"null" // 如果是从文件系统直接打开HTML文件Origin 会是 "null"仅用于开发
));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L); // 注意这里是 Long 类型
source.registerCorsConfiguration("/**", configuration); // 对所有路径生效
return source;
}
}

View File

@@ -5,6 +5,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -21,6 +22,7 @@ import java.io.IOException;
* JWT 登录授权过滤器
*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
@@ -36,10 +38,15 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
log.info("JWT 登录授权过滤器");
String authHeader = request.getHeader(this.tokenHeader);
log.info("authHeader: {}", authHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());
String authToken = authHeader.substring(this.tokenHead.length()+1);
log.info("authToken={}", authToken);
String username = jwtTokenUtil.getUsernameFromToken(authToken);
log.info("username={}", username);
// 如果 Token 中有用户名但上下文中没有,说明是首次登录
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

View File

@@ -23,6 +23,12 @@ public class AuthController {
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 注册
*
* @param registerRequest 注册信息
* @return 注册结果
*/
@PostMapping("/register")
public ApiResult<?> register(@RequestBody RegisterRequest registerRequest) {
if (authService.register(registerRequest) != null) {
@@ -30,7 +36,12 @@ public class AuthController {
}
return ApiResult.failed("用户名已存在");
}
/**
* 登录
*
* @param loginRequest 登录信息
* @return 登录结果
*/
@PostMapping("/login")
public ApiResult<?> login(@RequestBody LoginRequest loginRequest) {
String token = authService.login(loginRequest.getUsername(), loginRequest.getPassword());

View File

@@ -0,0 +1,73 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.ProcessVideoPath;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.VideoFileUploadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Map;
@RestController
@RequestMapping("/api/video")
@Slf4j
public class VideoController {
@Autowired
private VideoFileUploadService videoFileUploadService;
/**
* 初始化分片上传任务
* @param fileInfo 包含文件名(fileName)和总大小(totalSize)
* @return 返回 uploadId 和每个分片的大小
*/
@PostMapping("/init")
public ApiResult<?> initUpload(@RequestBody Map<String, Object> fileInfo) {
log.info("初始化上传任务: {}", fileInfo);
String fileName = fileInfo.get("fileName").toString();
long totalSize = Long.parseLong(fileInfo.get("totalSize").toString());
return ApiResult.success(videoFileUploadService.initUpload(fileName, totalSize));
}
/**
* 上传分片
* @param chunk 分片文件
* @param uploadId 上传任务ID
* @param chunkIndex 分片序号 (从0开始)
*/
@PostMapping("/chunk")
public ApiResult<?> uploadChunk(@RequestParam("chunk") MultipartFile chunk,
@RequestParam("uploadId") String uploadId,
@RequestParam("chunkIndex") int chunkIndex) throws IOException {
videoFileUploadService.uploadChunk(uploadId, chunkIndex, chunk.getBytes());
return ApiResult.success("分片 " + chunkIndex + " 上传成功");
}
/**
* 合并文件并触发异步处理
* @param params 包含 uploadId 和 fileName
*/
@PostMapping("/merge")
public ApiResult<?> mergeAndProcess(@RequestBody Map<String, String> params) throws IOException {
String uploadId = params.get("uploadId");
String fileName = params.get("fileName");
videoFileUploadService.mergeAndProcess(uploadId, fileName);
return ApiResult.success("文件上传成功,已加入后台处理队列。");
}
/**
* 处理视频
* @param processVideoPath 待处理的视频路径
*/
@PostMapping("/process")
public ApiResult<?> processVideo(@RequestBody ProcessVideoPath processVideoPath) throws IOException{
videoFileUploadService.pushResultToMQ(processVideoPath.getPath());
return ApiResult.success("视频处理任务已加入队列");
}
}

View File

@@ -42,6 +42,7 @@ public class AuthServiceImpl implements AuthService {
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
userMapper.insert(user);
// 你可以在这里为新用户分配默认角色
userMapper.insertUserRole(user.getId(), 2);
return user;
}

View File

@@ -5,6 +5,7 @@ import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
@@ -13,7 +14,9 @@ import org.springframework.security.core.userdetails.UserDetails;
import java.util.Date;
@Slf4j
@Component
public class JwtTokenUtil {
@@ -38,7 +41,9 @@ public class JwtTokenUtil {
*/
public String getUsernameFromToken(String token) {
try {
log.info("token:{};验证状态:{}",token,verifier.verify( token));
DecodedJWT jwt = verifier.verify(token);
log.info("从 Token 中获取用户名:{}", jwt.getSubject());
return jwt.getSubject();
} catch (Exception e) {
return null;

View File

@@ -15,6 +15,16 @@ spring:
min-idle: 5
max-active: 20
max-wait: 60000
rabbitmq:
host: 10.91.3.24
port: 5672
username: chaos
password: zx123456..
servlet:
multipart:
max-file-size: 100GB
max-request-size: 100GB
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
@@ -26,8 +36,17 @@ mybatis-plus:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
tokenHeader: Authorization
tokenHead: Chaos
secret: zHANgcHao@1995!20250506
expiration: 604800
expiration: 604800
file:
upload:
temp-dir: ./chaos/upload
ffmpeg-path: C:\Users\Chaos\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-7.1.1-full_build\bin\ffmpeg.exe