|
@@ -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 的处理方式)
|
|
* 已有关联资产的图片任务处理(参照 saveScriptProducts 的处理方式)
|
|
|
*
|
|
*
|
|
|
* - 已有图片任务的资产:收集任务ID,交给轮询
|
|
* - 已有图片任务的资产:收集任务ID,交给轮询
|
|
@@ -9468,6 +9623,10 @@ class AnimeService
|
|
|
|
|
|
|
|
$script_name = $script->script_name ?? 'script_' . $script_id;
|
|
$script_name = $script->script_name ?? 'script_' . $script_id;
|
|
|
|
|
|
|
|
|
|
+ // 先校验并提取本次入参中的新增资产行:
|
|
|
|
|
+ // 新增行的参考提示词为空时直接报错,不写任何数据、不创建任务中心记录
|
|
|
|
|
+ $newRows = $this->extractNewScriptProductRows($script_id, $products);
|
|
|
|
|
+
|
|
|
// 根据入参创建/复用任务中心记录(params md5 验重,避免重复任务)
|
|
// 根据入参创建/复用任务中心记录(params md5 验重,避免重复任务)
|
|
|
$taskCenterId = $this->createScriptProductTaskCenter(
|
|
$taskCenterId = $this->createScriptProductTaskCenter(
|
|
|
$uid,
|
|
$uid,
|
|
@@ -9485,7 +9644,8 @@ class AnimeService
|
|
|
->where('script_id', $script_id)
|
|
->where('script_id', $script_id)
|
|
|
->get();
|
|
->get();
|
|
|
|
|
|
|
|
- if ($existingMappings->isNotEmpty()) {
|
|
|
|
|
|
|
+ // 已有资产且本次没有新增行时,走原有的"已存在"处理;否则走下方创建逻辑
|
|
|
|
|
+ if ($existingMappings->isNotEmpty() && empty($newRows)) {
|
|
|
// 已存在资产映射,检查是否所有资产都有图片生成任务
|
|
// 已存在资产映射,检查是否所有资产都有图片生成任务
|
|
|
$productIds = $existingMappings->pluck('product_id')->unique()->toArray();
|
|
$productIds = $existingMappings->pluck('product_id')->unique()->toArray();
|
|
|
|
|
|
|
@@ -9653,17 +9813,40 @@ class AnimeService
|
|
|
return $resultData;
|
|
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;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 为当前类型创建根文件夹(如果还未创建)
|
|
|
|
|
|
|
+ // 为当前类型获取根文件夹:该剧本已存在则复用,避免重复创建根文件夹
|
|
|
if (!isset($typeFolderIds[$type])) {
|
|
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];
|
|
$folder_id = $typeFolderIds[$type];
|
|
@@ -9910,33 +10106,8 @@ class AnimeService
|
|
|
// 提交事务
|
|
// 提交事务
|
|
|
DB::commit();
|
|
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) {
|
|
} catch (\Exception $e) {
|
|
|
// 回滚事务
|
|
// 回滚事务
|
|
@@ -9960,6 +10131,52 @@ class AnimeService
|
|
|
// 统一使用 Utils::throwError 抛出错误
|
|
// 统一使用 Utils::throwError 抛出错误
|
|
|
Utils::throwError('20003:保存剧本资产失败:' . $e->getMessage());
|
|
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);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|