Ver Fonte

优化图片任务和视频任务更新逻辑,增加事务保证原子性

lh há 1 mês atrás
pai
commit
271c4b77e0

+ 67 - 70
app/Services/AIGeneration/AIImageGenerationService.php

@@ -98,6 +98,67 @@ class AIImageGenerationService
     }
     }
 
 
     /**
     /**
+     * 图片生成成功统一处理(事务保证原子性)
+     *
+     * 在同一事务内完成:任务状态更新 + 积分计费 + 分镜表同步;
+     * 任一步失败整体回滚,任务保持原状态,由后续定时任务/超时重置重试。
+     *
+     * @param MpGeneratePicTask $task
+     * @param array $resultUrls
+     * @param array $resultJson
+     * @return void
+     */
+    private function handleImageTaskSuccess(MpGeneratePicTask $task, array $resultUrls, array $resultJson): void
+    {
+        try {
+            DB::beginTransaction();
+
+            // 更新任务状态为成功
+            $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
+                'result_url' => $resultUrls,
+                'result_json' => $resultJson,
+            ]);
+
+            // 记录计费明细并扣减用户积分(真实失败则抛错回滚,避免“成功但未计费”)
+            $chargeResult = $this->pointsService->recordImageTaskCharge($task);
+            if (empty($chargeResult['charged'])
+                && !in_array((string)($chargeResult['reason'] ?? ''), ['already_charged', 'no_charge_info'], true)) {
+                throw new \Exception('图片计费失败: ' . (string)($chargeResult['reason'] ?? 'unknown'));
+            }
+
+            // 同步调整分镜图片状态和结果
+            $segmentId = getProp($task, 'alias_segment_id');
+            if ($segmentId) {
+                $updateSegmentResult = DB::table('mp_episode_segments')->where('segment_id', $segmentId)->update([
+                    'img_url' => $resultUrls[0],
+                    'pic_task_status' => '已完成',
+                ]);
+                // 本应更新却未更新到(分镜被删除等),抛错触发整体回滚重试
+                if ($updateSegmentResult === false || $updateSegmentResult === 0) {
+                    throw new \Exception('更新分镜图片状态失败: 未找到对应的分镜记录');
+                }
+
+                // 如果是首帧图ID则更新anime表
+                if (Redis::sismember('anime_first_frame_urls', $segmentId)) {
+                    $animeId = DB::table('mp_episode_segments')->where('segment_id', $segmentId)->value('anime_id');
+                    DB::table('mp_animes')->where('id', $animeId)->update(['first_frame_url' => $resultUrls[0], 'updated_at' => date('Y-m-d H:i:s')]);
+                    Redis::srem('anime_first_frame_urls', $segmentId);
+                }
+            }
+
+            DB::commit();
+
+        } catch (\Exception $e) {
+            DB::rollBack();
+            dLog('generate')->error('图片任务成功处理失败,已回滚: ' . $e->getMessage(), ['task_id' => $task->id]);
+            logDB('generate', 'error', '图片任务成功处理失败,已回滚', [
+                'task_id' => $task->id,
+                'error' => $e->getMessage(),
+            ]);
+        }
+    }
+
+    /**
      * 异步创建图片生成任务
      * 异步创建图片生成任务
      *
      *
      * @param array $params
      * @param array $params
@@ -634,30 +695,8 @@ class AIImageGenerationService
                 unset($imageData); // 解除引用
                 unset($imageData); // 解除引用
             }
             }
 
 
-            // 更新任务状态为成功
-            $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
-                'result_url' => $result_urls,
-                'result_json' => $cleanedResponseData // 保存清理后的数据
-            ]);
-
-            // 图片生成成功,记录计费明细并扣减用户积分
-            $this->pointsService->recordImageTaskCharge($task);
-
-            // 同步调整分镜图片状态和结果
-            $segment_id = getProp($task, 'alias_segment_id');
-            if ($segment_id) {
-                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
-                    'img_url' => $result_urls[0],
-                    'pic_task_status' => '已完成',
-                ]);
-                
-                // 如果是首帧图ID则更新anime表
-                if (Redis::sismember('anime_first_frame_urls', $segment_id)) {
-                    $anime_id = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('anime_id');
-                    DB::table('mp_animes')->where('id', $anime_id)->update(['first_frame_url' => $result_urls[0], 'updated_at' => date('Y-m-d H:i:s')]);
-                    Redis::srem('anime_first_frame_urls', $segment_id);
-                }
-            }
+            // 图片生成成功统一处理(事务:状态更新 + 计费 + 分镜同步)
+            $this->handleImageTaskSuccess($task, $result_urls, $cleanedResponseData);
         } catch (\Exception $e) {
         } catch (\Exception $e) {
             $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
             $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
                 'error_message' => '处理响应失败: ' . $e->getMessage()
                 'error_message' => '处理响应失败: ' . $e->getMessage()
@@ -806,30 +845,8 @@ class AIImageGenerationService
                 return;
                 return;
             }
             }
 
 
-            // 更新任务状态为成功
-            $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
-                'result_url' => $result_urls,
-                'result_json' => $responseData
-            ]);
-
-            // 图片生成成功,记录计费明细并扣减用户积分
-            $this->pointsService->recordImageTaskCharge($task);
-
-            // 同步调整分镜图片状态和结果
-            $segment_id = getProp($task, 'alias_segment_id');
-            if ($segment_id) {
-                DB::table('mp_episode_segments')->where('segment_id', $segment_id)->update([
-                    'img_url' => $result_urls[0],
-                    'pic_task_status' => '已完成',
-                ]);
-                
-                // 如果是首帧图ID则更新anime表
-                if (Redis::sismember('anime_first_frame_urls', $segment_id)) {
-                    $anime_id = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('anime_id');
-                    DB::table('mp_animes')->where('id', $anime_id)->update(['first_frame_url' => $result_urls[0], 'updated_at' => date('Y-m-d H:i:s')]);
-                    Redis::srem('anime_first_frame_urls', $segment_id);
-                }
-            }
+            // 图片生成成功统一处理(事务:状态更新 + 计费 + 分镜同步)
+            $this->handleImageTaskSuccess($task, $result_urls, $responseData);
         } catch (\Exception $e) {
         } catch (\Exception $e) {
             $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
             $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
                 'error_message' => '处理响应失败: ' . $e->getMessage()
                 'error_message' => '处理响应失败: ' . $e->getMessage()
@@ -1484,29 +1501,9 @@ class AIImageGenerationService
 
 
             if ($statusInfo['status'] === 'success') {
             if ($statusInfo['status'] === 'success') {
                 logDB('generate', 'info', '即梦AI图片生成任务成功', ['task_id' => $task->task_id, 'result_url' => $statusInfo['result_url']]);
                 logDB('generate', 'info', '即梦AI图片生成任务成功', ['task_id' => $task->task_id, 'result_url' => $statusInfo['result_url']]);
-                $task->updateStatus(MpGeneratePicTask::STATUS_SUCCESS, [
-                    'result_url'    => $statusInfo['result_url'],
-                    'result_json'   => $statusInfo['result_json'] ?? []
-                ]);
 
 
-                // 图片生成成功,记录计费明细并扣减用户积分
-                $this->pointsService->recordImageTaskCharge($task);
-
-                // 同步调整分镜图片状态和结果
-                $segment_id = getProp($task, 'alias_segment_id');
-                if ($segment_id) {
-                    DB::table('mp_episode_segments')->where('segment_id', getProp($task, 'alias_segment_id'))->update([
-                        'img_url'   => $statusInfo['result_url'][0],
-                        'pic_task_status'   => '已完成',
-                    ]);
-                    
-                    // 如果是首帧图ID则更新anime表
-                    if (Redis::sismember('anime_first_frame_urls', $segment_id)) {
-                        $anime_id = DB::table('mp_episode_segments')->where('segment_id', $segment_id)->value('anime_id');
-                        DB::table('mp_animes')->where('id', $anime_id)->update(['first_frame_url'=>$statusInfo['result_url'][0], 'updated_at'=>date('Y-m-d H:i:s')]);
-                        Redis::srem('anime_first_frame_urls', $segment_id);
-                    }
-                }
+                // 图片生成成功统一处理(事务:状态更新 + 计费 + 分镜同步)
+                $this->handleImageTaskSuccess($task, $statusInfo['result_url'], $statusInfo['result_json'] ?? []);
             } elseif ($statusInfo['status'] === 'failed') {
             } elseif ($statusInfo['status'] === 'failed') {
                 logDB('generate', 'error', '即梦AI图片生成任务失败', ['task_id' => $task->task_id, 'error' => $statusInfo['error_message']]);
                 logDB('generate', 'error', '即梦AI图片生成任务失败', ['task_id' => $task->task_id, 'error' => $statusInfo['error_message']]);
                 $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [
                 $task->updateStatus(MpGeneratePicTask::STATUS_FAILED, [

+ 116 - 30
app/Services/AIGeneration/AIVideoGenerationService.php

@@ -26,6 +26,21 @@ class AIVideoGenerationService
     }
     }
 
 
     /**
     /**
+     * 视频生成成功计费(事务内调用,真实失败抛错触发整体回滚)
+     *
+     * @param MpGenerateVideoTask $task
+     * @return void
+     */
+    private function chargeVideoTaskInTransaction(MpGenerateVideoTask $task): void
+    {
+        $result = $this->pointsService->recordVideoTaskCharge($task);
+        if (empty($result['charged'])
+            && !in_array((string)($result['reason'] ?? ''), ['already_charged', 'no_charge_info'], true)) {
+            throw new \Exception('视频计费失败: ' . (string)($result['reason'] ?? 'unknown'));
+        }
+    }
+
+    /**
      * 构建视频任务计费信息
      * 构建视频任务计费信息
      *
      *
      * @param array $params 创建任务参数
      * @param array $params 创建任务参数
@@ -410,11 +425,17 @@ class AIVideoGenerationService
                     }
                     }
                     
                     
                     // 更新分镜表(兼容act模式)
                     // 更新分镜表(兼容act模式)
-                    $this->updateSegmentTable($task, $segmentUpdateData);
+                    $updateSegmentResult = $this->updateSegmentTable($task, $segmentUpdateData);
+                    if ($updateSegmentResult === false || $updateSegmentResult === 0) {
+                        throw new \Exception('更新分镜表失败: 未找到对应的分镜记录');
+                    }
                     
                     
                     // 添加视频生成对话记录
                     // 添加视频生成对话记录
                     $this->addVideoGenerationRecords($task, $segment_id, $statusInfo['result_url']);
                     $this->addVideoGenerationRecords($task, $segment_id, $statusInfo['result_url']);
                     
                     
+                    // 视频生成成功,记录计费明细并扣减用户积分(与任务状态/分镜同事务)
+                    $this->chargeVideoTaskInTransaction($task);
+                    
                     DB::commit();
                     DB::commit();
                     
                     
                 } catch (\Exception $e) {
                 } catch (\Exception $e) {
@@ -427,11 +448,19 @@ class AIVideoGenerationService
                 }
                 }
             } else {
             } else {
                 // 没有segment_id的情况下,直接更新任务状态
                 // 没有segment_id的情况下,直接更新任务状态
-                $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+                try {
+                    DB::beginTransaction();
+                    $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+                    $this->chargeVideoTaskInTransaction($task);
+                    DB::commit();
+                } catch (\Exception $e) {
+                    DB::rollBack();
+                    dLog('generate')->error('视频任务成功处理失败,已回滚', [
+                        'task_id' => $task->id,
+                        'error' => $e->getMessage()
+                    ]);
+                }
             }
             }
-            
-            // 视频生成成功,记录计费明细并扣减用户积分
-            $this->pointsService->recordVideoTaskCharge($task);
         } elseif ($statusInfo['status'] === 'failed') {
         } elseif ($statusInfo['status'] === 'failed') {
             logDB('generate', 'error', '即梦AI视频生成任务失败', ['id' => $task->id, 'error' => $statusInfo['error_message']]);
             logDB('generate', 'error', '即梦AI视频生成任务失败', ['id' => $task->id, 'error' => $statusInfo['error_message']]);
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, $statusInfo);
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, $statusInfo);
@@ -687,11 +716,17 @@ class AIVideoGenerationService
                     }
                     }
                     
                     
                     // 更新分镜表(兼容act模式)
                     // 更新分镜表(兼容act模式)
-                    $this->updateSegmentTable($task, $segmentUpdateData);
+                    $updateSegmentResult = $this->updateSegmentTable($task, $segmentUpdateData);
+                    if ($updateSegmentResult === false || $updateSegmentResult === 0) {
+                        throw new \Exception('更新分镜表失败: 未找到对应的分镜记录');
+                    }
                     
                     
                     // 添加视频生成对话记录
                     // 添加视频生成对话记录
                     $this->addVideoGenerationRecords($task, $segment_id, $statusInfo['result_url']);
                     $this->addVideoGenerationRecords($task, $segment_id, $statusInfo['result_url']);
                     
                     
+                    // 视频生成成功,记录计费明细并扣减用户积分(与任务状态/分镜同事务)
+                    $this->chargeVideoTaskInTransaction($task);
+                    
                     DB::commit();
                     DB::commit();
                     
                     
                 } catch (\Exception $e) {
                 } catch (\Exception $e) {
@@ -704,11 +739,19 @@ class AIVideoGenerationService
                 }
                 }
             } else {
             } else {
                 // 没有segment_id的情况下,直接更新任务状态
                 // 没有segment_id的情况下,直接更新任务状态
-                $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+                try {
+                    DB::beginTransaction();
+                    $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+                    $this->chargeVideoTaskInTransaction($task);
+                    DB::commit();
+                } catch (\Exception $e) {
+                    DB::rollBack();
+                    dLog('generate')->error('视频任务成功处理失败,已回滚', [
+                        'task_id' => $task->id,
+                        'error' => $e->getMessage()
+                    ]);
+                }
             }
             }
-            
-            // 视频生成成功,记录计费明细并扣减用户积分
-            $this->pointsService->recordVideoTaskCharge($task);
         } elseif ($statusInfo['status'] === 'failed') {
         } elseif ($statusInfo['status'] === 'failed') {
             logDB('generate', 'error', 'Seedance视频生成任务失败', ['id' => $task->id, 'error' => $statusInfo['error_message']]);
             logDB('generate', 'error', 'Seedance视频生成任务失败', ['id' => $task->id, 'error' => $statusInfo['error_message']]);
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
@@ -1020,14 +1063,13 @@ class AIVideoGenerationService
                 }
                 }
             }
             }
             
             
-            // 更新任务记录
-            $task->update($updateData);
-            
-            // 如果任务成功,更新分镜表
+            // 更新任务记录(成功时与分镜同步、计费整体同事务)
             if ($taskStatus === MpGenerateVideoTask::STATUS_SUCCESS && isset($updateData['result_url'])) {
             if ($taskStatus === MpGenerateVideoTask::STATUS_SUCCESS && isset($updateData['result_url'])) {
                 try {
                 try {
                     DB::beginTransaction();
                     DB::beginTransaction();
                     
                     
+                    $task->update($updateData);
+                    
                     $now = date('Y-m-d H:i:s');
                     $now = date('Y-m-d H:i:s');
                     
                     
                     // 获取分镜ID(兼容分镜模式和全能模式)
                     // 获取分镜ID(兼容分镜模式和全能模式)
@@ -1102,6 +1144,9 @@ class AIVideoGenerationService
                         dLog('generate')->info('Seedance 分镜表更新成功: ' . $segmentId);
                         dLog('generate')->info('Seedance 分镜表更新成功: ' . $segmentId);
                     }
                     }
                     
                     
+                    // 视频生成成功,记录计费明细并扣减用户积分(与任务状态/分镜同事务)
+                    $this->chargeVideoTaskInTransaction($task);
+                    
                     DB::commit();
                     DB::commit();
                     
                     
                 } catch (\Exception $e) {
                 } catch (\Exception $e) {
@@ -1115,18 +1160,31 @@ class AIVideoGenerationService
                     logDB('generate', 'error', 'Seedance分镜表更新失败', $logData);
                     logDB('generate', 'error', 'Seedance分镜表更新失败', $logData);
                 }
                 }
             } elseif ($taskStatus === MpGenerateVideoTask::STATUS_FAILED) {
             } elseif ($taskStatus === MpGenerateVideoTask::STATUS_FAILED) {
-                // 任务失败,更新分镜表状态
+                // 任务失败:更新任务记录与分镜表状态
+                $task->update($updateData);
                 DB::table('mp_episode_segments')
                 DB::table('mp_episode_segments')
                     ->where('video_task_id', $task->id)
                     ->where('video_task_id', $task->id)
                     ->update([
                     ->update([
                         'video_task_status' => '失败',
                         'video_task_status' => '失败',
                         'updated_at' => date('Y-m-d H:i:s')
                         'updated_at' => date('Y-m-d H:i:s')
                     ]);
                     ]);
-            }
-            
-            // 视频生成成功,记录计费明细并扣减用户积分
-            if ($taskStatus === MpGenerateVideoTask::STATUS_SUCCESS) {
-                $this->pointsService->recordVideoTaskCharge($task);
+            } elseif ($taskStatus === MpGenerateVideoTask::STATUS_SUCCESS) {
+                // 成功但无结果URL:任务状态与计费同事务
+                try {
+                    DB::beginTransaction();
+                    $task->update($updateData);
+                    $this->chargeVideoTaskInTransaction($task);
+                    DB::commit();
+                } catch (\Exception $e) {
+                    DB::rollBack();
+                    dLog('generate')->error('视频任务成功处理失败,已回滚', [
+                        'task_id' => $task->id,
+                        'error' => $e->getMessage()
+                    ]);
+                }
+            } else {
+                // 处理中或其他状态:更新任务记录
+                $task->update($updateData);
             }
             }
             
             
             dLog('generate')->info('Seedance 回调处理完成: ' . $taskId . ', 状态: ' . $taskStatus);
             dLog('generate')->info('Seedance 回调处理完成: ' . $taskId . ', 状态: ' . $taskStatus);
@@ -1597,11 +1655,17 @@ class AIVideoGenerationService
                     }
                     }
                     
                     
                     // 更新分镜表(兼容act模式)
                     // 更新分镜表(兼容act模式)
-                    $this->updateSegmentTable($task, $segmentUpdateData);
+                    $updateSegmentResult = $this->updateSegmentTable($task, $segmentUpdateData);
+                    if ($updateSegmentResult === false || $updateSegmentResult === 0) {
+                        throw new \Exception('更新分镜表失败: 未找到对应的分镜记录');
+                    }
                     
                     
                     // 添加视频生成对话记录
                     // 添加视频生成对话记录
                     $this->addVideoGenerationRecords($task, $segment_id, $statusInfo['result_url']);
                     $this->addVideoGenerationRecords($task, $segment_id, $statusInfo['result_url']);
                     
                     
+                    // 视频生成成功,记录计费明细并扣减用户积分(与任务状态/分镜同事务)
+                    $this->chargeVideoTaskInTransaction($task);
+                    
                     DB::commit();
                     DB::commit();
                     
                     
                 } catch (\Exception $e) {
                 } catch (\Exception $e) {
@@ -1614,11 +1678,19 @@ class AIVideoGenerationService
                 }
                 }
             } else {
             } else {
                 // 没有segment_id的情况下,直接更新任务状态
                 // 没有segment_id的情况下,直接更新任务状态
-                $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+                try {
+                    DB::beginTransaction();
+                    $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+                    $this->chargeVideoTaskInTransaction($task);
+                    DB::commit();
+                } catch (\Exception $e) {
+                    DB::rollBack();
+                    dLog('generate')->error('视频任务成功处理失败,已回滚', [
+                        'task_id' => $task->id,
+                        'error' => $e->getMessage()
+                    ]);
+                }
             }
             }
-            
-            // 视频生成成功,记录计费明细并扣减用户积分
-            $this->pointsService->recordVideoTaskCharge($task);
         } elseif ($statusInfo['status'] === 'failed') {
         } elseif ($statusInfo['status'] === 'failed') {
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
                 'error_message' => $statusInfo['error_message'],
                 'error_message' => $statusInfo['error_message'],
@@ -2717,7 +2789,10 @@ class AIVideoGenerationService
                     }
                     }
                     
                     
                     // 更新分镜表(兼容act模式)
                     // 更新分镜表(兼容act模式)
-                    $this->updateSegmentTable($task, $segmentUpdateData);
+                    $updateSegmentResult = $this->updateSegmentTable($task, $segmentUpdateData);
+                    if ($updateSegmentResult === false || $updateSegmentResult === 0) {
+                        throw new \Exception('更新分镜表失败: 未找到对应的分镜记录');
+                    }
                     
                     
                     // 添加视频生成对话记录
                     // 添加视频生成对话记录
                     $this->addVideoGenerationRecords($task, $segment_id, $statusInfo['result_url']);
                     $this->addVideoGenerationRecords($task, $segment_id, $statusInfo['result_url']);
@@ -2747,6 +2822,9 @@ class AIVideoGenerationService
                         }
                         }
                     }
                     }
                     
                     
+                    // 视频生成成功,记录计费明细并扣减用户积分(与任务状态/分镜同事务)
+                    $this->chargeVideoTaskInTransaction($task);
+                    
                     DB::commit();
                     DB::commit();
                     // 从Redis中移除
                     // 从Redis中移除
                     Redis::srem('anime_act_first_frame_urls', $act_id);
                     Redis::srem('anime_act_first_frame_urls', $act_id);
@@ -2761,11 +2839,19 @@ class AIVideoGenerationService
                 }
                 }
             } else {
             } else {
                 // 没有segment_id的情况下,直接更新任务状态
                 // 没有segment_id的情况下,直接更新任务状态
-                $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+                try {
+                    DB::beginTransaction();
+                    $task->updateStatus(MpGenerateVideoTask::STATUS_SUCCESS, $statusInfo);
+                    $this->chargeVideoTaskInTransaction($task);
+                    DB::commit();
+                } catch (\Exception $e) {
+                    DB::rollBack();
+                    dLog('generate')->error('视频任务成功处理失败,已回滚', [
+                        'task_id' => $task->id,
+                        'error' => $e->getMessage()
+                    ]);
+                }
             }
             }
-            
-            // 视频生成成功,记录计费明细并扣减用户积分
-            $this->pointsService->recordVideoTaskCharge($task);
         } elseif ($statusInfo['status'] === 'failed') {
         } elseif ($statusInfo['status'] === 'failed') {
             logDB('generate', 'error', '统一API视频生成任务失败', ['id' => $task->id, 'error' => $statusInfo['error_message']]);
             logDB('generate', 'error', '统一API视频生成任务失败', ['id' => $task->id, 'error' => $statusInfo['error_message']]);
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [
             $task->updateStatus(MpGenerateVideoTask::STATUS_FAILED, [