2026-04-14 13:47:38 +08:00
|
|
|
|
package com.label.controller;
|
2026-04-09 16:18:39 +08:00
|
|
|
|
|
|
|
|
|
|
import com.label.common.result.Result;
|
|
|
|
|
|
import com.label.common.shiro.TokenPrincipal;
|
2026-04-14 13:39:24 +08:00
|
|
|
|
import com.label.entity.VideoProcessJob;
|
2026-04-14 13:45:15 +08:00
|
|
|
|
import com.label.service.VideoProcessService;
|
2026-04-12 00:15:59 +08:00
|
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
2026-04-09 16:18:39 +08:00
|
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
|
import org.apache.shiro.authz.annotation.RequiresRoles;
|
2026-04-09 19:42:20 +08:00
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
2026-04-09 16:18:39 +08:00
|
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-14 13:31:50 +08:00
|
|
|
|
* 视频处理接口(4 个端点)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* POST /api/video/process — 触发视频处理(ADMIN)
|
|
|
|
|
|
* GET /api/video/jobs/{jobId} — 查询任务状态(ADMIN)
|
|
|
|
|
|
* POST /api/video/jobs/{jobId}/reset — 重置失败任务(ADMIN)
|
|
|
|
|
|
* POST /api/video/callback — AI 回调接口(无需认证,已在 TokenFilter 中排除)
|
2026-04-09 16:18:39 +08:00
|
|
|
|
*/
|
2026-04-14 13:31:50 +08:00
|
|
|
|
@Tag(name = "视频处理", description = "视频处理任务创建、查询、重置和回调")
|
2026-04-09 16:18:39 +08:00
|
|
|
|
@Slf4j
|
|
|
|
|
|
@RestController
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
public class VideoController {
|
|
|
|
|
|
|
|
|
|
|
|
private final VideoProcessService videoProcessService;
|
|
|
|
|
|
|
2026-04-09 19:42:20 +08:00
|
|
|
|
@Value("${video.callback-secret:}")
|
|
|
|
|
|
private String callbackSecret;
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
/** POST /api/video/process — 触发视频处理任务 */
|
|
|
|
|
|
@Operation(summary = "触发视频处理任务")
|
2026-04-09 16:18:39 +08:00
|
|
|
|
@PostMapping("/api/video/process")
|
|
|
|
|
|
@RequiresRoles("ADMIN")
|
|
|
|
|
|
public Result<VideoProcessJob> createJob(@RequestBody Map<String, Object> body,
|
|
|
|
|
|
HttpServletRequest request) {
|
2026-04-09 19:42:20 +08:00
|
|
|
|
Object sourceIdVal = body.get("sourceId");
|
|
|
|
|
|
Object jobTypeVal = body.get("jobType");
|
|
|
|
|
|
if (sourceIdVal == null || jobTypeVal == null) {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
return Result.failure("INVALID_PARAMS", "sourceId 和 jobType 不能为空");
|
2026-04-09 19:42:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
Long sourceId = Long.parseLong(sourceIdVal.toString());
|
|
|
|
|
|
String jobType = jobTypeVal.toString();
|
2026-04-09 16:18:39 +08:00
|
|
|
|
String params = body.containsKey("params") ? body.get("params").toString() : null;
|
|
|
|
|
|
|
|
|
|
|
|
TokenPrincipal principal = principal(request);
|
|
|
|
|
|
return Result.success(
|
|
|
|
|
|
videoProcessService.createJob(sourceId, jobType, params, principal.getCompanyId()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
/** GET /api/video/jobs/{jobId} — 查询视频处理任务 */
|
|
|
|
|
|
@Operation(summary = "查询视频处理任务状态")
|
2026-04-09 16:18:39 +08:00
|
|
|
|
@GetMapping("/api/video/jobs/{jobId}")
|
|
|
|
|
|
@RequiresRoles("ADMIN")
|
|
|
|
|
|
public Result<VideoProcessJob> getJob(@PathVariable Long jobId,
|
|
|
|
|
|
HttpServletRequest request) {
|
|
|
|
|
|
return Result.success(videoProcessService.getJob(jobId, principal(request).getCompanyId()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
/** POST /api/video/jobs/{jobId}/reset — 管理员重置失败任务 */
|
|
|
|
|
|
@Operation(summary = "重置失败的视频处理任务")
|
2026-04-09 16:18:39 +08:00
|
|
|
|
@PostMapping("/api/video/jobs/{jobId}/reset")
|
|
|
|
|
|
@RequiresRoles("ADMIN")
|
|
|
|
|
|
public Result<VideoProcessJob> resetJob(@PathVariable Long jobId,
|
|
|
|
|
|
HttpServletRequest request) {
|
|
|
|
|
|
return Result.success(videoProcessService.reset(jobId, principal(request).getCompanyId()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-14 13:31:50 +08:00
|
|
|
|
* POST /api/video/callback — AI 服务回调(无需 Bearer Token)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 此端点已在 TokenFilter.shouldNotFilter() 中排除认证,
|
|
|
|
|
|
* 由 AI 服务直接调用,携带 jobId、status、outputPath 等参数。
|
|
|
|
|
|
*
|
|
|
|
|
|
* Body 示例:
|
|
|
|
|
|
* { "jobId": 123, "status": "SUCCESS", "outputPath": "processed/123/frames.zip" }
|
2026-04-09 16:18:39 +08:00
|
|
|
|
* { "jobId": 123, "status": "FAILED", "errorMessage": "ffmpeg error: ..." }
|
|
|
|
|
|
*/
|
2026-04-14 13:31:50 +08:00
|
|
|
|
@Operation(summary = "接收 AI 服务视频处理回调")
|
2026-04-09 16:18:39 +08:00
|
|
|
|
@PostMapping("/api/video/callback")
|
2026-04-09 19:42:20 +08:00
|
|
|
|
public Result<Void> handleCallback(@RequestBody Map<String, Object> body,
|
2026-04-12 00:15:59 +08:00
|
|
|
|
HttpServletRequest request) {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
// 共享密钥校验(配置了 VIDEO_CALLBACK_SECRET 时强制校验)
|
2026-04-09 19:42:20 +08:00
|
|
|
|
if (callbackSecret != null && !callbackSecret.isBlank()) {
|
|
|
|
|
|
String provided = request.getHeader("X-Callback-Secret");
|
|
|
|
|
|
if (!callbackSecret.equals(provided)) {
|
2026-04-14 13:31:50 +08:00
|
|
|
|
return Result.failure("UNAUTHORIZED", "回调密钥无效");
|
2026-04-09 19:42:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-09 16:18:39 +08:00
|
|
|
|
Long jobId = Long.parseLong(body.get("jobId").toString());
|
|
|
|
|
|
String status = (String) body.get("status");
|
|
|
|
|
|
String outputPath = body.containsKey("outputPath") ? (String) body.get("outputPath") : null;
|
|
|
|
|
|
String errorMessage = body.containsKey("errorMessage") ? (String) body.get("errorMessage") : null;
|
|
|
|
|
|
|
2026-04-14 13:31:50 +08:00
|
|
|
|
log.info("视频处理回调:jobId={}, status={}", jobId, status);
|
2026-04-09 16:18:39 +08:00
|
|
|
|
videoProcessService.handleCallback(jobId, status, outputPath, errorMessage);
|
|
|
|
|
|
return Result.success(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private TokenPrincipal principal(HttpServletRequest request) {
|
|
|
|
|
|
return (TokenPrincipal) request.getAttribute("__token_principal__");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|