unknown 5 روز پیش
والد
کامیت
528e8fbc5c
1فایلهای تغییر یافته به همراه311 افزوده شده و 90 حذف شده
  1. 311 90
      application/api/controller/WorkOrder.php

+ 311 - 90
application/api/controller/WorkOrder.php

@@ -340,12 +340,14 @@ class WorkOrder extends Api{
 
 
         $firstImageUrl = $this->finalizeFrameImageToOss($firstFrame, $taskId);
         $firstImageUrl = $this->finalizeFrameImageToOss($firstFrame, $taskId);
         $lastImageUrl = $this->finalizeFrameImageToOss($lastFrame, $taskId);
         $lastImageUrl = $this->finalizeFrameImageToOss($lastFrame, $taskId);
+        $firstImageDb = $this->toDbUploadPath($firstImageUrl);
+        $lastImageDb = $this->toDbUploadPath($lastImageUrl);
 
 
         $videoData = [
         $videoData = [
             'video_id' => $taskId,
             'video_id' => $taskId,
             'prompt' => $prompt,
             'prompt' => $prompt,
-            'first_image_url' => $firstImageUrl,
-            'last_image_url' => $lastImageUrl,
+            'first_image_url' => $firstImageDb,
+            'last_image_url' => $lastImageDb,
             'model' => $data['model'],
             'model' => $data['model'],
             'seconds' => (string)$data['duration'],
             'seconds' => (string)$data['duration'],
             'size' => (string)$data['ratio'],
             'size' => (string)$data['ratio'],
@@ -374,15 +376,14 @@ class WorkOrder extends Api{
                 'status' => $responseData['status'] ?? '',
                 'status' => $responseData['status'] ?? '',
                 'created_at' => $responseData['created_at'] ?? '',
                 'created_at' => $responseData['created_at'] ?? '',
                 'mode' => $lastImageUrl !== '' ? 'first_last_frame' : 'single_frame',
                 'mode' => $lastImageUrl !== '' ? 'first_last_frame' : 'single_frame',
-                'first_image_url' => $firstImageUrl,
-                'last_image_url' => $lastImageUrl,
+                'first_image_url' => $this->toPublicMediaUrl($firstImageDb),
+                'last_image_url' => $this->toPublicMediaUrl($lastImageDb),
             ]
             ]
         ]);
         ]);
     }
     }
 
 
     /**
     /**
-     * 即梦AI--获取视频接口
-     * 首帧图 + 尾帧图 = 新效果视频
+     * 即梦AI--获取视频接口(先 GET 查询任务状态,succeeded 后再落盘/OSS/入库)
      */
      */
     public function Get_ImgToVideo()
     public function Get_ImgToVideo()
     {
     {
@@ -390,93 +391,74 @@ class WorkOrder extends Api{
         $apiKey = 'ark-1ca8aa97-3663-4bc7-8c53-d4ab516883f1-d2339';
         $apiKey = 'ark-1ca8aa97-3663-4bc7-8c53-d4ab516883f1-d2339';
 
 
         $params = $this->request->param();
         $params = $this->request->param();
-        $taskId = $params['task_id'] ?? $params['video_id'] ?? '';
+        $taskId = trim((string)($params['task_id'] ?? $params['video_id'] ?? ''));
         if ($taskId === '') {
         if ($taskId === '') {
             return json(['code' => 0, 'msg' => '任务 ID 不能为空']);
             return json(['code' => 0, 'msg' => '任务 ID 不能为空']);
         }
         }
 
 
-        // 查询任务状态
-        $queryUrl = $apiUrl . '/' . $taskId;
-        $ch2 = curl_init();
-        curl_setopt($ch2, CURLOPT_URL, $queryUrl);
-        curl_setopt($ch2, CURLOPT_HTTPHEADER, [
-            'Content-Type: application/json',
-            'Authorization: Bearer ' . $apiKey
-        ]);
-        curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
-        curl_setopt($ch2, CURLOPT_SSL_VERIFYPEER, false); // 开发环境临时关闭SSL验证
-        curl_setopt($ch2, CURLOPT_TIMEOUT, 60); // 超时时间
-
-        $queryResponse = curl_exec($ch2);
-
-        // 检查 cURL 错误
-        if (curl_errno($ch2)) {
-            $error = curl_error($ch2);
-            curl_close($ch2);
-            return json(['code' => 0, 'msg' => 'Curl 错误: ' . $error]);
+        // 1. 先根据任务 ID 查询即梦任务状态(GET /tasks/{id})
+        $queryResult = $this->fetchVolcImgToVideoTask($taskId, $apiUrl, $apiKey);
+        if (!$queryResult['ok']) {
+            return json([
+                'code' => 0,
+                'msg' => '查询任务失败: ' . $queryResult['error'],
+                'data' => [
+                    'task_id' => $taskId,
+                    'http_code' => $queryResult['http_code'],
+                    'task' => $queryResult['data'],
+                ],
+            ]);
         }
         }
-        curl_close($ch2);
 
 
-        // 解析查询响应
-        $queryData = json_decode($queryResponse, true);
-        // print_r($queryData);die;
-
-        // 轮询任务状态,直到完成
-        $maxPolls = 30;
-        $pollCount = 0;
-        $pollData = is_array($queryData) ? $queryData : [];
-        $taskStatus = $pollData['status'] ?? '';
+        $taskData = $queryResult['data'];
+        $taskStatus = (string)($taskData['status'] ?? '');
 
 
-        while (!in_array($taskStatus, ['completed', 'succeeded']) && $pollCount < $maxPolls) {
-            sleep(5); // 每5秒轮询一次
-            $pollCount++;
-
-            // 再次查询任务状态
-            $ch3 = curl_init();
-            curl_setopt($ch3, CURLOPT_URL, $queryUrl);
-            curl_setopt($ch3, CURLOPT_HTTPHEADER, [
-                'Content-Type: application/json',
-                'Authorization: Bearer ' . $apiKey
+        // 2. 终态(failed / cancelled / expired)直接返回,不进入下载/入库
+        $terminalMsg = $this->resolveVolcTaskTerminalFailure($taskStatus);
+        if ($terminalMsg !== null) {
+            return json([
+                'code' => 0,
+                'msg' => $terminalMsg,
+                'data' => $this->formatVolcTaskQueryPayload($taskData),
             ]);
             ]);
-            curl_setopt($ch3, CURLOPT_RETURNTRANSFER, true);
-            curl_setopt($ch3, CURLOPT_SSL_VERIFYPEER, false);
-            curl_setopt($ch3, CURLOPT_TIMEOUT, 60);
-
-            $pollResponse = curl_exec($ch3);
-            curl_close($ch3);
-
-            $pollData = json_decode($pollResponse, true);
-            $taskStatus = $pollData['status'] ?? '';
-
-            // 检查任务是否失败
-            if ($taskStatus === 'failed') {
-                return json(['code' => 0, 'msg' => '任务执行失败']);
-            }
         }
         }
 
 
-        // 如果任务已经成功,直接使用 $queryData
-        if (empty($pollData) && isset($queryData['status']) && $queryData['status'] === 'succeeded') {
-            $pollData = $queryData;
+        // 3. queued / running 等未完成态则轮询(最多 30 次,间隔 5 秒)
+        if (!$this->isVolcVideoTaskSucceeded($taskStatus)) {
+            $pollResult = $this->pollVolcImgToVideoTaskUntilDone($taskId, $apiUrl, $apiKey, 30, 5);
+            if (!$pollResult['ok']) {
+                return json([
+                    'code' => 0,
+                    'msg' => $pollResult['error'],
+                    'data' => $this->formatVolcTaskQueryPayload($pollResult['data']),
+                ]);
+            }
+            $taskData = $pollResult['data'];
+            $taskStatus = (string)($taskData['status'] ?? '');
         }
         }
 
 
-        // 检查轮询是否超时
-        if (!in_array($taskStatus, ['completed', 'succeeded'])) {
-            return json(['code' => 0, 'msg' => '任务执行超时']);
+        if (!$this->isVolcVideoTaskSucceeded($taskStatus)) {
+            return json([
+                'code' => 0,
+                'msg' => '轮询超时,当前状态:' . $this->getVolcTaskStatusMessage($taskStatus),
+                'data' => $this->formatVolcTaskQueryPayload($taskData),
+            ]);
         }
         }
 
 
-        // 获取视频 URL(兼容不同响应结构)
-        $videoUrl = $pollData['content']['video_url']
-            ?? $pollData['output']['video_url']
-            ?? $pollData['video_url']
-            ?? '';
+        // 4. 事务处理:下载视频 → 本地/OSS → 更新数据库
+        $videoUrl = $this->extractVideoUrlFromVolcTask($taskData);
         if ($videoUrl === '') {
         if ($videoUrl === '') {
-            return json(['code' => 0, 'msg' => '获取视频 URL 失败', 'data' => ['pollData' => $pollData]]);
+            return json([
+                'code' => 0,
+                'msg' => '获取视频 URL 失败',
+                'data' => $this->formatVolcTaskQueryPayload($taskData),
+            ]);
         }
         }
 
 
         $fileName = $this->sanitizeTaskIdSegment($taskId) . '.mp4';
         $fileName = $this->sanitizeTaskIdSegment($taskId) . '.mp4';
         $saveDir = $this->buildTaskMediaLocalDir($taskId);
         $saveDir = $this->buildTaskMediaLocalDir($taskId);
-        if (!is_dir($saveDir)) {
-            mkdir($saveDir, 0755, true);
+        if (!is_dir($saveDir) && !@mkdir($saveDir, 0755, true) && !is_dir($saveDir)) {
+            return json(['code' => 0, 'msg' => '创建视频目录失败', 'data' => ['saveDir' => $saveDir]]);
         }
         }
 
 
         $savePath = $saveDir . $fileName;
         $savePath = $saveDir . $fileName;
@@ -491,42 +473,222 @@ class WorkOrder extends Api{
 
 
         $objectKey = $this->buildTaskMediaObjectKey($taskId, $fileName);
         $objectKey = $this->buildTaskMediaObjectKey($taskId, $fileName);
         $upload = $this->uploadToOSS($savePath, $objectKey);
         $upload = $this->uploadToOSS($savePath, $objectKey);
-        if ($upload['success']) {
-            $webUrl = $upload['url'] !== '' ? $upload['url'] : Common::ossFullUrl($objectKey);
-        } else {
-            $webUrl = $objectKey;
-            Log::write('[Get_ImgToVideo] OSS上传失败,使用本地相对路径: ' . $objectKey, 'error');
+        $ossObjectKey = (string)($upload['object_key'] ?? $objectKey);
+        $webUrlDb = $this->toDbUploadPath($ossObjectKey);
+        if ($webUrlDb === '' || stripos($webUrlDb, 'uploads/') !== 0) {
+            $fullUrl = $upload['success']
+                ? ((string)($upload['url'] ?? '') !== '' ? $upload['url'] : Common::ossFullUrl($objectKey))
+                : ($this->buildPublicUploadUrl($savePath) ?: Common::ossFullUrl($objectKey));
+            $webUrlDb = $this->toDbUploadPath((string)$fullUrl);
+            if (!$upload['success']) {
+                Log::write('[Get_ImgToVideo] OSS上传失败,入库路径=' . $webUrlDb, 'error');
+            }
         }
         }
 
 
         try {
         try {
-            Db::name('video')->where('video_id', $taskId)->update(['web_url' => $webUrl]);
+            Db::name('video')->where('video_id', $taskId)->update(['web_url' => $webUrlDb]);
         } catch (Exception $e) {
         } catch (Exception $e) {
             return json([
             return json([
                 'code' => 0,
                 'code' => 0,
                 'msg' => '视频已生成,但数据库更新失败',
                 'msg' => '视频已生成,但数据库更新失败',
-                'data' => [
-                    'task_id' => $taskId,
-                    'web_url' => $webUrl,
+                'data' => array_merge($this->formatVolcTaskQueryPayload($taskData), [
+                    'web_url' => $this->toPublicMediaUrl($webUrlDb),
                     'error_message' => $e->getMessage(),
                     'error_message' => $e->getMessage(),
-                ],
+                ]),
             ]);
             ]);
         }
         }
 
 
         return json([
         return json([
             'code' => 1,
             'code' => 1,
             'msg' => '视频获取成功',
             'msg' => '视频获取成功',
-            'data' => [
+            'data' => array_merge($this->formatVolcTaskQueryPayload($taskData), [
                 'task_id' => $taskId,
                 'task_id' => $taskId,
                 'video_id' => $taskId,
                 'video_id' => $taskId,
-                'status' => $taskStatus,
-                'web_url' => $webUrl,
-                'oss_object_key' => $upload['object_key'] ?? $objectKey,
+                'web_url' => $this->toPublicMediaUrl($webUrlDb),
+                'oss_object_key' => $ossObjectKey,
                 'oss_uploaded' => $upload['success'] ?? false,
                 'oss_uploaded' => $upload['success'] ?? false,
-                'local_path' => $objectKey,
-            ],
+                'local_path' => $webUrlDb,
+                'source_video_url' => $videoUrl,
+            ]),
         ]);
         ]);
     }
     }
 
 
+    /**
+     * GET 查询即梦图生视频任务:/api/v3/contents/generations/tasks/{task_id}
+     * @return array{ok:bool,data:array,error:string,http_code:int}
+     */
+    private function fetchVolcImgToVideoTask(string $taskId, string $apiUrl, string $apiKey): array
+    {
+        $empty = ['ok' => false, 'data' => [], 'error' => '', 'http_code' => 0];
+        $queryUrl = rtrim($apiUrl, '/') . '/' . str_replace('%2F', '/', rawurlencode($taskId));
+
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $queryUrl);
+        curl_setopt($ch, CURLOPT_HTTPGET, true);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, [
+            'Content-Type: application/json',
+            'Authorization: Bearer ' . $apiKey,
+        ]);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_TIMEOUT, 60);
+
+        $response = curl_exec($ch);
+        if (curl_errno($ch)) {
+            $empty['error'] = curl_error($ch);
+            curl_close($ch);
+            return $empty;
+        }
+
+        $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+
+        $data = json_decode((string)$response, true);
+        if (!is_array($data)) {
+            return ['ok' => false, 'data' => [], 'error' => '任务响应解析失败', 'http_code' => $httpCode];
+        }
+
+        if (isset($data['error'])) {
+            $errMsg = is_array($data['error'])
+                ? (string)($data['error']['message'] ?? json_encode($data['error'], JSON_UNESCAPED_UNICODE))
+                : (string)$data['error'];
+            return ['ok' => false, 'data' => $data, 'error' => $errMsg, 'http_code' => $httpCode];
+        }
+
+        if ($httpCode < 200 || $httpCode >= 300) {
+            return ['ok' => false, 'data' => $data, 'error' => 'HTTP ' . $httpCode, 'http_code' => $httpCode];
+        }
+
+        return ['ok' => true, 'data' => $data, 'error' => '', 'http_code' => $httpCode];
+    }
+
+    /**
+     * 轮询任务直至 succeeded,或遇到 failed/cancelled/expired
+     * @return array{ok:bool,data:array,error:string}
+     */
+    private function pollVolcImgToVideoTaskUntilDone(
+        string $taskId,
+        string $apiUrl,
+        string $apiKey,
+        int $maxPolls = 30,
+        int $intervalSeconds = 5
+    ): array {
+        $pollCount = 0;
+        $lastData = [];
+
+        while ($pollCount < $maxPolls) {
+            sleep($intervalSeconds);
+            $pollCount++;
+
+            $result = $this->fetchVolcImgToVideoTask($taskId, $apiUrl, $apiKey);
+            if (!$result['ok']) {
+                return ['ok' => false, 'data' => $result['data'], 'error' => $result['error']];
+            }
+
+            $lastData = $result['data'];
+            $status = (string)($lastData['status'] ?? '');
+
+            $terminalMsg = $this->resolveVolcTaskTerminalFailure($status);
+            if ($terminalMsg !== null) {
+                return ['ok' => false, 'data' => $lastData, 'error' => $terminalMsg];
+            }
+            if ($this->isVolcVideoTaskSucceeded($status)) {
+                return ['ok' => true, 'data' => $lastData, 'error' => ''];
+            }
+        }
+
+        $lastStatus = (string)($lastData['status'] ?? '');
+        return [
+            'ok' => false,
+            'data' => $lastData,
+            'error' => '轮询超时,当前状态:' . $this->getVolcTaskStatusMessage($lastStatus),
+        ];
+    }
+
+    /**
+     * 即梦任务 status 中文说明(官方:queued/running/cancelled/succeeded/failed/expired)
+     */
+    private function getVolcTaskStatusMessage(string $status): string
+    {
+        $map = [
+            'queued' => '排队中',
+            'running' => '任务运行中',
+            'cancelled' => '任务已取消',
+            'succeeded' => '任务成功',
+            'failed' => '任务失败',
+            'expired' => '任务超时',
+            'completed' => '任务成功',
+        ];
+        return $map[$status] ?? ('未知状态(' . $status . ')');
+    }
+
+    /**
+     * 是否为可继续轮询的进行中状态
+     */
+    private function isVolcVideoTaskPending(string $status): bool
+    {
+        return in_array($status, ['queued', 'running'], true);
+    }
+
+    /**
+     * 终态且不可下载:failed / cancelled / expired
+     */
+    private function resolveVolcTaskTerminalFailure(string $status): ?string
+    {
+        $map = [
+            'failed' => '任务执行失败',
+            'cancelled' => '任务已取消',
+            'expired' => '任务超时',
+        ];
+        return $map[$status] ?? null;
+    }
+
+    private function isVolcVideoTaskSucceeded(string $status): bool
+    {
+        return in_array($status, ['succeeded', 'completed'], true);
+    }
+
+    /**
+     * 从即梦任务响应中取视频地址(优先 content.video_url)
+     */
+    private function extractVideoUrlFromVolcTask(array $taskData): string
+    {
+        $content = $taskData['content'] ?? null;
+        if (is_array($content) && !empty($content['video_url'])) {
+            return trim((string)$content['video_url']);
+        }
+        return trim((string)(
+            $taskData['output']['video_url']
+            ?? $taskData['video_url']
+            ?? ''
+        ));
+    }
+
+    /**
+     * 格式化任务查询结果(与即梦 GET 响应字段对齐)
+     */
+    private function formatVolcTaskQueryPayload(array $taskData): array
+    {
+        $status = (string)($taskData['status'] ?? '');
+        return [
+            'id' => $taskData['id'] ?? '',
+            'model' => $taskData['model'] ?? '',
+            'status' => $status,
+            'status_text' => $this->getVolcTaskStatusMessage($status),
+            'is_pending' => $this->isVolcVideoTaskPending($status),
+            'is_succeeded' => $this->isVolcVideoTaskSucceeded($status),
+            'content' => $taskData['content'] ?? null,
+            'usage' => $taskData['usage'] ?? null,
+            'created_at' => $taskData['created_at'] ?? null,
+            'updated_at' => $taskData['updated_at'] ?? null,
+            'resolution' => $taskData['resolution'] ?? '',
+            'ratio' => $taskData['ratio'] ?? '',
+            'duration' => $taskData['duration'] ?? null,
+            'framespersecond' => $taskData['framespersecond'] ?? null,
+            'generate_audio' => $taskData['generate_audio'] ?? null,
+        ];
+    }
+
     /**
     /**
      * 下载远程文件(视频等)
      * 下载远程文件(视频等)
      * @return string|false
      * @return string|false
@@ -572,6 +734,18 @@ class WorkOrder extends Api{
             ->order('id desc')
             ->order('id desc')
             ->limit(($page - 1) * $limit, $limit)
             ->limit(($page - 1) * $limit, $limit)
             ->select();
             ->select();
+        foreach ($list as &$row) {
+            if (!empty($row['web_url'])) {
+                $row['web_url'] = $this->toPublicMediaUrl((string)$row['web_url']);
+            }
+            if (!empty($row['first_image_url'])) {
+                $row['first_image_url'] = $this->toPublicMediaUrl((string)$row['first_image_url']);
+            }
+            if (!empty($row['last_image_url'])) {
+                $row['last_image_url'] = $this->toPublicMediaUrl((string)$row['last_image_url']);
+            }
+        }
+        unset($row);
         $total = Db::name('video')->where('mod_rq', null)
         $total = Db::name('video')->where('mod_rq', null)
             ->where($where)
             ->where($where)
             ->count();
             ->count();
@@ -1732,6 +1906,53 @@ class WorkOrder extends Api{
         ];
         ];
     }
     }
 
 
+    /**
+     * 入库路径:仅保留 uploads/ 及后面部分
+     */
+    private function toDbUploadPath(string $path): string
+    {
+        $path = trim(str_replace('\\', '/', $path));
+        if ($path === '') {
+            return '';
+        }
+
+        if (stripos($path, 'http://') === 0 || stripos($path, 'https://') === 0) {
+            $mark = '/uploads/';
+            $pos = stripos($path, $mark);
+            if ($pos !== false) {
+                return ltrim(substr($path, $pos + 1), '/');
+            }
+            return $path;
+        }
+
+        $path = ltrim($path, '/');
+        if (stripos($path, 'uploads/') === 0) {
+            return $path;
+        }
+
+        $pos = stripos($path, 'uploads/');
+        if ($pos !== false) {
+            return substr($path, $pos);
+        }
+
+        return $path;
+    }
+
+    /**
+     * 接口返回:库内相对路径转完整 URL
+     */
+    private function toPublicMediaUrl(string $dbPath): string
+    {
+        $dbPath = trim($dbPath);
+        if ($dbPath === '') {
+            return '';
+        }
+        if (stripos($dbPath, 'http://') === 0 || stripos($dbPath, 'https://') === 0) {
+            return $dbPath;
+        }
+        return Common::ossFullUrl($this->toDbUploadPath($dbPath));
+    }
+
     /**
     /**
      * 将 public 下本地文件转为可公网访问的站点 URL(OSS 不可用时的回退)
      * 将 public 下本地文件转为可公网访问的站点 URL(OSS 不可用时的回退)
      */
      */