Просмотр исходного кода

优化保存剧本资产逻辑,增加新资产验证及生成图片任务

lh 5 дней назад
Родитель
Сommit
050676f726
1 измененных файлов с 266 добавлено и 49 удалено
  1. 266 49
      app/Services/Anime/AnimeService.php

+ 266 - 49
app/Services/Anime/AnimeService.php

@@ -9272,6 +9272,161 @@ class AnimeService
     }
 
     /**
+     * 获取剧本已关联的资产名称(仅按剧本判重,不区分资产类型)
+     *
+     * @param int $script_id
+     * @return array ['资产名称' => true]
+     */
+    private function getScriptAssetNames($script_id) {
+        $products = DB::table('mp_script_product_mappings as m')
+            ->join('mp_products as p', 'm.product_id', '=', 'p.id')
+            ->where('m.script_id', $script_id)
+            ->where('p.type', 1)
+            ->where('p.is_deleted', 0)
+            ->get(['p.product_name']);
+
+        $names = [];
+        foreach ($products as $product) {
+            $name = $this->normalizeImportProductName($product->product_name);
+            if ($name !== '') {
+                $names[$name] = true;
+            }
+        }
+
+        return $names;
+    }
+
+    /**
+     * 校验并提取本次入参中的新增资产行
+     *
+     * 规则:
+     * - 资产名称为空(或仅剩占位符):跳过
+     * - 同一份入参内重名(跨类型):保留最后一条
+     * - 与剧本已关联资产同名(跨类型判重,仅限当前剧本):不算新增
+     * - 新增行的参考提示词为空:直接报错,不进行任何后续处理
+     *
+     * @param int   $script_id
+     * @param mixed $products 入参(数组或 JSON 字符串)
+     * @return array [['type' => int, 'product_name' => string, 'pic_prompt' => string, 'episode_numbers' => []]]
+     */
+    private function extractNewScriptProductRows($script_id, $products) {
+        // 兼容 JSON 字符串;无法解析时按"没有新增行"处理,由后续创建逻辑给出原有报错
+        if (is_string($products)) {
+            $decoded = json_decode($products, true);
+            $products = (json_last_error() === JSON_ERROR_NONE) ? $decoded : [];
+        }
+
+        if (!is_array($products) || empty($products)) {
+            return [];
+        }
+
+        $rowMap = [];
+        foreach ($products as $productGroup) {
+            $type = getProp($productGroup, 'type');
+            $content = getProp($productGroup, 'content', []);
+
+            if (!in_array($type, [1, 2, 3]) || empty($content) || count($content) < 2) {
+                continue;
+            }
+
+            // 获取表头(第一行)并查找关键列的位置
+            $headers = $content[0];
+            $nameColumnIndex = -1;
+            $sequenceColumnIndex = -1;
+            $promptColumnIndex = -1;
+            $fallbackPromptColumnIndex = -1;
+
+            foreach ($headers as $index => $header) {
+                if (strpos($header, '资产名称') !== false || strpos($header, '名称') !== false) {
+                    $nameColumnIndex = $index;
+                }
+                if (strpos($header, '使用范围') !== false || strpos($header, '范围') !== false) {
+                    $sequenceColumnIndex = $index;
+                }
+                if (strpos($header, '参考') !== false && strpos($header, '提示词') !== false) {
+                    $promptColumnIndex = $index;
+                } elseif (strpos($header, '提示词') !== false && $fallbackPromptColumnIndex === -1) {
+                    // 与 Excel 导入保持一致:优先"参考提示词",否则兼容"提示词"列
+                    $fallbackPromptColumnIndex = $index;
+                }
+            }
+
+            if ($promptColumnIndex === -1) {
+                $promptColumnIndex = $fallbackPromptColumnIndex;
+            }
+
+            if ($nameColumnIndex === -1) {
+                continue;
+            }
+
+            for ($i = 1; $i < count($content); $i++) {
+                $row = $content[$i];
+
+                if (empty($row) || !isset($row[$nameColumnIndex])) {
+                    continue;
+                }
+
+                // 处理product_name两边的符号;名称为空或仅剩占位符号则跳过
+                $product_name = $this->normalizeImportProductName($row[$nameColumnIndex]);
+                if ($product_name === '') {
+                    continue;
+                }
+
+                $pic_prompt = '';
+                if ($promptColumnIndex >= 0 && isset($row[$promptColumnIndex])) {
+                    $pic_prompt = trim((string)$row[$promptColumnIndex]);
+                }
+
+                // 解析使用范围(逗号分隔的序号),为空时由创建逻辑写入默认映射(序号0)
+                $sequences = [];
+                $sequence_str = ($sequenceColumnIndex >= 0 && isset($row[$sequenceColumnIndex]))
+                    ? trim((string)$row[$sequenceColumnIndex])
+                    : '';
+                if ($sequence_str !== '') {
+                    foreach (array_map('trim', explode(',', $sequence_str)) as $part) {
+                        if (is_numeric($part)) {
+                            $sequences[] = (int)$part;
+                        }
+                    }
+                }
+
+                // 跨类型判重,同一份入参内重名保留最后一条
+                $rowMap[$product_name] = [
+                    'type'            => (int)$type,
+                    'product_name'    => $product_name,
+                    'pic_prompt'      => $pic_prompt,
+                    'episode_numbers' => $sequences,
+                ];
+            }
+        }
+
+        // 与剧本已关联资产判重(仅限当前剧本,跨类型)
+        $existingNames = $this->getScriptAssetNames($script_id);
+        $newRows = [];
+        $emptyPromptNames = [];
+
+        foreach ($rowMap as $name => $row) {
+            if (isset($existingNames[$name])) {
+                continue;
+            }
+
+            if ($row['pic_prompt'] === '') {
+                $emptyPromptNames[] = $name;
+                continue;
+            }
+
+            $newRows[] = $row;
+        }
+
+        // 新增行的参考提示词为空:直接报错,不进行任何后续处理
+        if (!empty($emptyPromptNames)) {
+            Utils::throwError('20003:以下新增资产的参考提示词不能为空:' . implode('、', $emptyPromptNames));
+        }
+
+        return $newRows;
+    }
+
+    /**
      * 已有关联资产的图片任务处理(参照 saveScriptProducts 的处理方式)
      *
      * - 已有图片任务的资产:收集任务ID,交给轮询
@@ -9468,6 +9623,10 @@ class AnimeService
         
         $script_name = $script->script_name ?? 'script_' . $script_id;
         
+        // 先校验并提取本次入参中的新增资产行:
+        // 新增行的参考提示词为空时直接报错,不写任何数据、不创建任务中心记录
+        $newRows = $this->extractNewScriptProductRows($script_id, $products);
+
         // 根据入参创建/复用任务中心记录(params md5 验重,避免重复任务)
         $taskCenterId = $this->createScriptProductTaskCenter(
             $uid,
@@ -9485,7 +9644,8 @@ class AnimeService
             ->where('script_id', $script_id)
             ->get();
         
-        if ($existingMappings->isNotEmpty()) {
+        // 已有资产且本次没有新增行时,走原有的"已存在"处理;否则走下方创建逻辑
+        if ($existingMappings->isNotEmpty() && empty($newRows)) {
             // 已存在资产映射,检查是否所有资产都有图片生成任务
             $productIds = $existingMappings->pluck('product_id')->unique()->toArray();
             
@@ -9653,17 +9813,40 @@ class AnimeService
             return $resultData;
         }
 
-        // 解析 products(可能是 JSON 字符串或数组)
-        if (is_string($products)) {
-            $products = json_decode($products, true);
-            if (json_last_error() !== JSON_ERROR_NONE) {
-                Utils::throwError('20003:产品数据格式错误');
+        // 走到这里说明有新行需要创建(或剧本还没有任何资产),保留原有的入参校验报错
+        if (empty($newRows)) {
+            $decodedProducts = $products;
+            if (is_string($decodedProducts)) {
+                $decodedProducts = json_decode($decodedProducts, true);
+                if (json_last_error() !== JSON_ERROR_NONE) {
+                    Utils::throwError('20003:产品数据格式错误');
+                }
             }
+
+            if (!is_array($decodedProducts) || empty($decodedProducts)) {
+                Utils::throwError('20003:产品数据不能为空');
+            }
+
+            Utils::throwError('20003:没有有效的产品数据');
         }
 
-        if (!is_array($products) || empty($products)) {
-            Utils::throwError('20003:产品数据不能为空');
+        // 仅保留本次新增的资产行,复用下方原有的创建逻辑
+        $products = [];
+        foreach ($newRows as $newRow) {
+            if (!isset($products[$newRow['type']])) {
+                $products[$newRow['type']] = [
+                    'type'    => $newRow['type'],
+                    'content' => [['资产名称', '使用范围', '参考提示词']],
+                ];
+            }
+
+            $products[$newRow['type']]['content'][] = [
+                $newRow['product_name'],
+                implode(',', $newRow['episode_numbers']),
+                $newRow['pic_prompt'],
+            ];
         }
+        $products = array_values($products);
 
         // 以下是原有的创建逻辑...
         // 开启事务
@@ -9690,21 +9873,34 @@ class AnimeService
                     continue;
                 }
                 
-                // 为当前类型创建根文件夹(如果还未创建)
+                // 为当前类型获取根文件夹:该剧本已存在则复用,避免重复创建根文件夹
                 if (!isset($typeFolderIds[$type])) {
-                    $typeFolderIds[$type] = DB::table('mp_products')->insertGetId([
-                        'user_id' => $uid,
-                        'cpid' => $cpid,
-                        'type' => 2, // 1.资产 2文件夹
-                        'product' => $type, // 1.主体 2.场景 3.道具
-                        'parent_id' => 0, // 根文件夹,parent_id=0
-                        'level' => 1, // 第一层级
-                        'product_name' => $script_name,
-                        'sort_order' => time() + $type,
-                        'is_deleted' => 0,
-                        'created_at' => $now,
-                        'updated_at' => $now
-                    ]);
+                    $folder = DB::table('mp_products')
+                        ->where('cpid', $cpid)
+                        ->where('type', 2)
+                        ->where('product', $type)
+                        ->where('product_name', $script_name)
+                        ->where('parent_id', 0)
+                        ->where('is_deleted', 0)
+                        ->first();
+
+                    if ($folder) {
+                        $typeFolderIds[$type] = $folder->id;
+                    } else {
+                        $typeFolderIds[$type] = DB::table('mp_products')->insertGetId([
+                            'user_id' => $uid,
+                            'cpid' => $cpid,
+                            'type' => 2, // 1.资产 2文件夹
+                            'product' => $type, // 1.主体 2.场景 3.道具
+                            'parent_id' => 0, // 根文件夹,parent_id=0
+                            'level' => 1, // 第一层级
+                            'product_name' => $script_name,
+                            'sort_order' => time() + $type,
+                            'is_deleted' => 0,
+                            'created_at' => $now,
+                            'updated_at' => $now
+                        ]);
+                    }
                 }
                 
                 $folder_id = $typeFolderIds[$type];
@@ -9910,33 +10106,8 @@ class AnimeService
             // 提交事务
             DB::commit();
             
-            // 事务提交成功后,如果有图片生成任务,则返回 SSE 流
-            if ($auto_generate_images && !empty($productTaskMap)) {
-                // 返回 SSE 流(事务外轮询)
-                return $this->pollProductImageTasks($script_id, $script_name, $productTaskMap, $typeFolderIds, $taskCenterId);
-            }
-            
-            // 如果不需要生成图片,返回普通结果
-            $resultData = [
-                'success' => true,
-                'type_folder_ids' => $typeFolderIds,
-                'inserted_count' => $insertedCount,
-                'total_records' => count($mappingRecords),
-                'created_products' => count($createdProductIds),
-                'image_tasks_created' => count($productTaskMap),
-                'task_center_id' => $taskCenterId
-            ];
-
-            // 接口已正常完成,更新任务中心状态和结果
-            if ($taskCenterId) {
-                $this->taskCenterService->updateTask($taskCenterId, [
-                    'status' => MpTaskCenter::STATUS_SUCCESS,
-                    'result' => json_encode($resultData, JSON_UNESCAPED_UNICODE),
-                    'error_message' => null
-                ]);
-            }
-
-            return $resultData;
+            // 本次新增资产创建的图片任务
+            $createdTaskMap = $productTaskMap;
             
         } catch (\Exception $e) {
             // 回滚事务
@@ -9960,6 +10131,52 @@ class AnimeService
             // 统一使用 Utils::throwError 抛出错误
             Utils::throwError('20003:保存剧本资产失败:' . $e->getMessage());
         }
+
+        // 落库成功后统一处理图片任务:
+        // 1) 本次新增资产的图片任务已在上方事务中创建
+        // 2) 剧本内已存在但缺图的资产在此补建,并与新增资产的任务一并收集为待轮询任务
+        $existingTaskInfo = $this->prepareExistingScriptProductTasks(
+            $script_id,
+            $model,
+            $width,
+            $height,
+            $auto_generate_images
+        );
+
+        $productTaskMap = $existingTaskInfo['product_task_map'];
+        // 创建阶段解析到的类型根文件夹优先
+        $typeFolderIds = $typeFolderIds + $existingTaskInfo['type_folder_ids'];
+
+        // 保存结果(SSE 的 init / complete 事件会携带该结构)
+        $resultData = [
+            'success'             => true,
+            'message'             => '保存完成',
+            'script_id'           => $script_id,
+            'script_name'         => $script_name,
+            'type_folder_ids'     => $typeFolderIds,
+            'inserted_count'      => $insertedCount,
+            'total_records'       => count($mappingRecords),
+            'created_products'    => count($createdProductIds),
+            'pending_tasks_count' => count($productTaskMap),
+            'image_tasks_created' => count($createdTaskMap) + $existingTaskInfo['created_count'],
+            'task_center_id'      => $taskCenterId,
+        ];
+
+        // 有待完成的图片任务:返回 SSE 轮询流(事务外轮询)
+        if ($auto_generate_images && !empty($productTaskMap)) {
+            return $this->pollProductImageTasks(
+                $script_id,
+                $script_name,
+                $productTaskMap,
+                $typeFolderIds,
+                $taskCenterId,
+                ['import_result' => $resultData],
+                ['import_result' => $resultData]
+            );
+        }
+
+        // 没有需要轮询的任务:通过 SSE complete 事件返回保存结果
+        return $this->streamImportComplete($script_id, $script_name, $resultData, $typeFolderIds, $taskCenterId);
     }
 
     /**