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