|
|
@@ -6353,6 +6353,7 @@ class AnimeService
|
|
|
$cpid = Site::getCpid();
|
|
|
$script_id = getProp($data, 'script_id');
|
|
|
$products = getProp($data, 'products', []);
|
|
|
+ $auto_generate_images = getProp($data, 'auto_generate_images', true); // 是否自动生成图片,默认true
|
|
|
|
|
|
if (!$script_id) {
|
|
|
Utils::throwError('20003:请提供剧本ID');
|
|
|
@@ -6367,6 +6368,171 @@ class AnimeService
|
|
|
if (!$script) {
|
|
|
Utils::throwError('20003:剧本不存在');
|
|
|
}
|
|
|
+
|
|
|
+ $script_name = $script->script_name ?? 'script_' . $script_id;
|
|
|
+
|
|
|
+ // 检查是否已经有该剧本的资产数据
|
|
|
+ $existingMappings = DB::table('mp_script_product_mappings')
|
|
|
+ ->where('script_id', $script_id)
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ if ($existingMappings->isNotEmpty()) {
|
|
|
+ // 已存在资产映射,检查是否所有资产都有图片生成任务
|
|
|
+ $productIds = $existingMappings->pluck('product_id')->unique()->toArray();
|
|
|
+
|
|
|
+ // 查询这些资产的图片生成任务状态
|
|
|
+ $productsWithTasks = DB::table('mp_products')
|
|
|
+ ->whereIn('id', $productIds)
|
|
|
+ ->where('is_deleted', 0)
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ $productTaskMap = [];
|
|
|
+ $typeFolderIds = [];
|
|
|
+ $hasAllTasks = true;
|
|
|
+
|
|
|
+ foreach ($productsWithTasks as $product) {
|
|
|
+ // 收集类型文件夹ID
|
|
|
+ if ($product->parent_id > 0 && !in_array($product->parent_id, $typeFolderIds)) {
|
|
|
+ $parentFolder = DB::table('mp_products')
|
|
|
+ ->where('id', $product->parent_id)
|
|
|
+ ->where('type', 2)
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if ($parentFolder) {
|
|
|
+ $typeFolderIds[$parentFolder->product] = $parentFolder->id;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否有图片生成任务
|
|
|
+ if (!empty($product->pic_task_id)) {
|
|
|
+ $productTaskMap[$product->id] = $product->pic_task_id;
|
|
|
+ } else {
|
|
|
+ // 有资产没有图片任务
|
|
|
+ $hasAllTasks = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果所有资产都有任务ID,直接轮询返回结果
|
|
|
+ if ($hasAllTasks && !empty($productTaskMap)) {
|
|
|
+ dLog('anime')->info('剧本资产已存在且都有图片任务,直接轮询结果', [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'script_name' => $script_name,
|
|
|
+ 'product_count' => count($productTaskMap)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 直接返回 SSE 流轮询结果
|
|
|
+ return $this->pollProductImageTasks($script_id, $script_name, $productTaskMap, $typeFolderIds);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果部分资产没有任务ID,只为这些资产创建任务
|
|
|
+ if (!$hasAllTasks && $auto_generate_images) {
|
|
|
+ dLog('anime')->info('剧本资产已存在,但部分资产没有图片任务,为缺失的资产创建任务', [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'script_name' => $script_name
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 开启事务
|
|
|
+ DB::beginTransaction();
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 默认图片生成参数
|
|
|
+ $model = 'doubao-seedream-5-0-lite-260128';
|
|
|
+ $width = 1600;
|
|
|
+ $height = 2848;
|
|
|
+
|
|
|
+ foreach ($productsWithTasks as $product) {
|
|
|
+ // 跳过已有任务的资产
|
|
|
+ if (!empty($product->pic_task_id)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 跳过没有提示词的资产
|
|
|
+ if (empty($product->pic_prompt)) {
|
|
|
+ dLog('anime')->warning('资产没有提示词,跳过图片生成', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'product_name' => $product->product_name
|
|
|
+ ]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建图片生成任务
|
|
|
+ $params = [
|
|
|
+ 'prompt' => $product->pic_prompt,
|
|
|
+ 'model' => $model,
|
|
|
+ 'ref_img_urls' => [],
|
|
|
+ 'width' => $width,
|
|
|
+ 'height' => $height
|
|
|
+ ];
|
|
|
+
|
|
|
+ $task = $this->aiImageGenerationService->createImageGenerationTask($params);
|
|
|
+ $task_id = $task->id;
|
|
|
+
|
|
|
+ if ($task_id) {
|
|
|
+ $productTaskMap[$product->id] = $task_id;
|
|
|
+
|
|
|
+ // 在事务中更新资产表的任务ID和状态
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_id' => $task_id,
|
|
|
+ 'pic_task_status' => '生成中',
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ dLog('anime')->info('为已存在资产创建图片生成任务', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'task_id' => $task_id
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('anime')->error('创建资产图片生成任务失败', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 标记为失败(仍在事务中)
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_status' => '生成失败',
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交事务
|
|
|
+ DB::commit();
|
|
|
+
|
|
|
+ // 如果有新创建的任务,返回 SSE 流
|
|
|
+ if (!empty($productTaskMap)) {
|
|
|
+ return $this->pollProductImageTasks($script_id, $script_name, $productTaskMap, $typeFolderIds);
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ // 回滚事务
|
|
|
+ DB::rollback();
|
|
|
+
|
|
|
+ dLog('anime')->error('为已存在资产创建图片任务失败', [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ Utils::throwError('20003:创建图片任务失败:' . $e->getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果不需要生成图片,返回已存在的信息
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'message' => '剧本资产已存在',
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'script_name' => $script_name,
|
|
|
+ 'type_folder_ids' => $typeFolderIds,
|
|
|
+ 'existing_products' => count($productIds),
|
|
|
+ 'tasks_count' => count($productTaskMap)
|
|
|
+ ];
|
|
|
+ }
|
|
|
|
|
|
// 解析 products(可能是 JSON 字符串或数组)
|
|
|
if (is_string($products)) {
|
|
|
@@ -6380,63 +6546,577 @@ class AnimeService
|
|
|
Utils::throwError('20003:产品数据不能为空');
|
|
|
}
|
|
|
|
|
|
- // 验证每个 product_id 是否存在
|
|
|
- $productIds = array_column($products, 'product_id');
|
|
|
- $validProductIds = DB::table('mp_products')
|
|
|
- ->where('cpid', $cpid)
|
|
|
- // ->where('user_id', $uid)
|
|
|
- ->where('is_deleted', 0)
|
|
|
- ->whereIn('id', $productIds)
|
|
|
- ->pluck('id')
|
|
|
- ->toArray();
|
|
|
+ // 以下是原有的创建逻辑...
|
|
|
+ // 开启事务
|
|
|
+ DB::beginTransaction();
|
|
|
+
|
|
|
+ try {
|
|
|
+ $now = now();
|
|
|
+ $mappingRecords = [];
|
|
|
+ $createdProductIds = []; // 记录所有创建的资产ID
|
|
|
+
|
|
|
+ // 获取剧本名称
|
|
|
+ $script_name = $script->script_name ?? 'script_' . $script_id;
|
|
|
+
|
|
|
+ // 存储每个类型的根文件夹ID
|
|
|
+ $typeFolderIds = [];
|
|
|
+
|
|
|
+ // 默认图片生成参数
|
|
|
+ $model = 'doubao-seedream-5-0-lite-260128';
|
|
|
+ $width = 1600;
|
|
|
+ $height = 2848;
|
|
|
+
|
|
|
+ // 遍历每个产品类型(type: 1=角色, 2=场景, 3=道具)
|
|
|
+ foreach ($products as $productGroup) {
|
|
|
+ $type = getProp($productGroup, 'type');
|
|
|
+ $title = getProp($productGroup, 'title', '');
|
|
|
+ $content = getProp($productGroup, 'content', []);
|
|
|
+
|
|
|
+ if (!in_array($type, [1, 2, 3]) || empty($content) || count($content) < 2) {
|
|
|
+ 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_id = $typeFolderIds[$type];
|
|
|
+
|
|
|
+ // 获取表头(第一行)并查找关键列的位置
|
|
|
+ $headers = $content[0];
|
|
|
+ $nameColumnIndex = -1; // 资产名称列索引
|
|
|
+ $sequenceColumnIndex = -1; // 使用范围列索引
|
|
|
+ $promptColumnIndex = -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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证必要的列是否都找到了
|
|
|
+ if ($nameColumnIndex === -1) {
|
|
|
+ dLog('anime')->warning('未找到资产名称列', ['type' => $type, 'headers' => $headers]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if ($sequenceColumnIndex === -1) {
|
|
|
+ dLog('anime')->warning('未找到使用范围列', ['type' => $type, 'headers' => $headers]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 跳过表头,解析每一行数据
|
|
|
+ for ($i = 1; $i < count($content); $i++) {
|
|
|
+ $row = $content[$i];
|
|
|
+
|
|
|
+ // 确保行数据有效
|
|
|
+ if (empty($row) || count($row) <= max($nameColumnIndex, $sequenceColumnIndex)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据动态查找的列索引提取数据
|
|
|
+ $product_name = isset($row[$nameColumnIndex]) ? trim($row[$nameColumnIndex]) : '';
|
|
|
+ $sequence_str = isset($row[$sequenceColumnIndex]) ? trim($row[$sequenceColumnIndex]) : '';
|
|
|
+ $pic_prompt = '';
|
|
|
+
|
|
|
+ // 提取参考提示词(如果找到了该列)
|
|
|
+ if ($promptColumnIndex >= 0 && isset($row[$promptColumnIndex])) {
|
|
|
+ $pic_prompt = trim($row[$promptColumnIndex]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析使用范围(逗号分隔的序号)
|
|
|
+ $sequences = [];
|
|
|
+ if (!empty($sequence_str)) {
|
|
|
+ $sequenceParts = array_map('trim', explode(',', $sequence_str));
|
|
|
+ foreach ($sequenceParts as $part) {
|
|
|
+ if (is_numeric($part)) {
|
|
|
+ $sequences[] = (int)$part;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (empty($product_name)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 直接插入新资产到 mp_products 表(parent_id 指向对应类型的根文件夹)
|
|
|
+ $product_id = DB::table('mp_products')->insertGetId([
|
|
|
+ 'product_name' => $product_name,
|
|
|
+ 'type' => 1, // 1=资产 2.文件夹
|
|
|
+ 'product' => $type, // 1=角色, 2=场景, 3=道具
|
|
|
+ 'parent_id' => $folder_id, // 父文件夹是该类型的根文件夹
|
|
|
+ 'level' => 2, // 第二层级
|
|
|
+ 'pic_prompt' => $pic_prompt,
|
|
|
+ 'user_id' => $uid,
|
|
|
+ 'cpid' => $cpid,
|
|
|
+ 'sort_order' => time(),
|
|
|
+ 'is_deleted' => 0,
|
|
|
+ 'created_at' => $now,
|
|
|
+ 'updated_at' => $now,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 记录创建的资产ID
|
|
|
+ $createdProductIds[] = $product_id;
|
|
|
+
|
|
|
+ // 为每个序号创建映射记录
|
|
|
+ if (!empty($sequences)) {
|
|
|
+ foreach ($sequences as $sequence) {
|
|
|
+ $mappingRecords[] = [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'product_id' => $product_id,
|
|
|
+ 'episode_number' => $sequence,
|
|
|
+ 'created_at' => $now,
|
|
|
+ 'updated_at' => $now,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果没有指定序号,创建一个默认映射(序号为0)
|
|
|
+ $mappingRecords[] = [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'product_id' => $product_id,
|
|
|
+ 'episode_number' => 0,
|
|
|
+ 'created_at' => $now,
|
|
|
+ 'updated_at' => $now,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 检查是否有无效的 product_id
|
|
|
- $invalidIds = array_diff($productIds, $validProductIds);
|
|
|
- if (!empty($invalidIds)) {
|
|
|
- Utils::throwError('20003:以下资产ID不存在或无权访问 ' . implode(', ', $invalidIds));
|
|
|
- }
|
|
|
+ if (empty($mappingRecords)) {
|
|
|
+ Utils::throwError('20003:没有有效的产品数据');
|
|
|
+ }
|
|
|
|
|
|
- // 准备插入数据
|
|
|
- $records = [];
|
|
|
- $now = now();
|
|
|
+ // 批量插入映射数据(使用 insertOrIgnore 避免重复插入)
|
|
|
+ $insertedCount = DB::table('mp_script_product_mappings')->insertOrIgnore($mappingRecords);
|
|
|
+
|
|
|
+ // 如果需要自动生成图片,在事务中创建图片生成任务
|
|
|
+ $productTaskMap = [];
|
|
|
+ if ($auto_generate_images && !empty($createdProductIds)) {
|
|
|
+ // 查询所有创建的资产
|
|
|
+ $productsToGenerate = DB::table('mp_products')
|
|
|
+ ->whereIn('id', $createdProductIds)
|
|
|
+ ->where('is_deleted', 0)
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ foreach ($productsToGenerate as $product) {
|
|
|
+ if (empty($product->pic_prompt)) {
|
|
|
+ dLog('anime')->warning('资产没有提示词,跳过图片生成', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'product_name' => $product->product_name
|
|
|
+ ]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建图片生成任务
|
|
|
+ $params = [
|
|
|
+ 'prompt' => $product->pic_prompt,
|
|
|
+ 'model' => $model,
|
|
|
+ 'ref_img_urls' => [],
|
|
|
+ 'width' => $width,
|
|
|
+ 'height' => $height
|
|
|
+ ];
|
|
|
+
|
|
|
+ $task = $this->aiImageGenerationService->createImageGenerationTask($params);
|
|
|
+ $task_id = $task->id;
|
|
|
+
|
|
|
+ if ($task_id) {
|
|
|
+ $productTaskMap[$product->id] = $task_id;
|
|
|
+
|
|
|
+ // 在事务中更新资产表的任务ID和状态
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_id' => $task_id,
|
|
|
+ 'pic_task_status' => '生成中',
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ dLog('anime')->info('创建资产图片生成任务', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'task_id' => $task_id
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('anime')->error('创建资产图片生成任务失败', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 标记为失败(仍在事务中)
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_status' => '生成失败',
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交事务
|
|
|
+ DB::commit();
|
|
|
+
|
|
|
+ // 事务提交成功后,如果有图片生成任务,则返回 SSE 流
|
|
|
+ if ($auto_generate_images && !empty($productTaskMap)) {
|
|
|
+ // 返回 SSE 流(事务外轮询)
|
|
|
+ return $this->pollProductImageTasks($script_id, $script_name, $productTaskMap, $typeFolderIds);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果不需要生成图片,返回普通结果
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'type_folder_ids' => $typeFolderIds,
|
|
|
+ 'inserted_count' => $insertedCount,
|
|
|
+ 'total_records' => count($mappingRecords),
|
|
|
+ 'created_products' => count($createdProductIds),
|
|
|
+ 'image_tasks_created' => count($productTaskMap)
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ // 回滚事务
|
|
|
+ DB::rollback();
|
|
|
+
|
|
|
+ // 记录错误日志
|
|
|
+ dLog('anime')->error('保存剧本资产失败: ' . $e->getMessage(), [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'trace' => $e->getTraceAsString()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 统一使用 Utils::throwError 抛出错误
|
|
|
+ Utils::throwError('20003:保存剧本资产失败:' . $e->getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
+ /**
|
|
|
+ * 批量为剧本资产生成图片
|
|
|
+ *
|
|
|
+ * @param array $data 请求参数
|
|
|
+ * @return \Generator 返回 SSE 流
|
|
|
+ */
|
|
|
+ public function batchGenerateProductImages($data) {
|
|
|
+ $script_id = getProp($data, 'script_id');
|
|
|
+ $type_folder_ids = getProp($data, 'type_folder_ids', []); // {1: 角色文件夹ID, 2: 场景文件夹ID, 3: 道具文件夹ID}
|
|
|
+
|
|
|
+ if (!$script_id) {
|
|
|
+ Utils::throwError('20003:请提供剧本ID');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证剧本是否存在
|
|
|
+ $script = DB::table('mp_scripts')
|
|
|
+ ->where('id', $script_id)
|
|
|
+ ->where('is_deleted', 0)
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if (!$script) {
|
|
|
+ Utils::throwError('20003:剧本不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ $script_name = $script->script_name ?? 'script_' . $script_id;
|
|
|
+
|
|
|
+ // 获取需要生成图片的所有资产
|
|
|
+ $products = DB::table('mp_products')
|
|
|
+ ->whereIn('parent_id', array_values($type_folder_ids))
|
|
|
+ ->where('is_deleted', 0)
|
|
|
+ ->whereIn('type', [1, 2, 3]) // 只处理资产,不处理文件夹
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ if ($products->isEmpty()) {
|
|
|
+ Utils::throwError('20003:没有找到需要生成图片的资产');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认参数
|
|
|
+ $model = 'doubao-seedream-5-0-lite-260128';
|
|
|
+ $width = 1600;
|
|
|
+ $height = 2848;
|
|
|
+
|
|
|
+ // 开始为每个资产创建图片生成任务
|
|
|
+ $taskIds = [];
|
|
|
+ $productTaskMap = []; // 资产ID到任务ID的映射
|
|
|
+
|
|
|
foreach ($products as $product) {
|
|
|
- if (!isset($product['product_id']) || !isset($product['episode_number'])) {
|
|
|
+ if (empty($product->pic_prompt)) {
|
|
|
+ dLog('anime')->warning('资产没有提示词,跳过', ['product_id' => $product->id, 'product_name' => $product->product_name]);
|
|
|
continue;
|
|
|
}
|
|
|
-
|
|
|
- $productId = $product['product_id'];
|
|
|
- $episodeNumbers = $product['episode_number'];
|
|
|
-
|
|
|
- // 如果 episode_number 是数组,则展开成多条记录
|
|
|
- if (is_array($episodeNumbers)) {
|
|
|
- foreach ($episodeNumbers as $episodeNumber) {
|
|
|
- $records[] = [
|
|
|
- 'script_id' => $script_id,
|
|
|
- 'product_id' => $productId,
|
|
|
- 'episode_number' => $episodeNumber,
|
|
|
- 'created_at' => $now,
|
|
|
- 'updated_at' => $now,
|
|
|
- ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建图片生成任务
|
|
|
+ $params = [
|
|
|
+ 'prompt' => $product->pic_prompt,
|
|
|
+ 'model' => $model,
|
|
|
+ 'ref_img_urls' => [],
|
|
|
+ 'width' => $width,
|
|
|
+ 'height' => $height
|
|
|
+ ];
|
|
|
+
|
|
|
+ $task = $this->aiImageGenerationService->createImageGenerationTask($params);
|
|
|
+ $task_id = $task->id;
|
|
|
+
|
|
|
+ if ($task_id) {
|
|
|
+ $taskIds[] = $task_id;
|
|
|
+ $productTaskMap[$product->id] = $task_id;
|
|
|
+
|
|
|
+ // 更新资产表的任务ID和状态
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_id' => $task_id,
|
|
|
+ 'pic_task_status' => '生成中',
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ dLog('anime')->info('创建资产图片生成任务', ['product_id' => $product->id, 'task_id' => $task_id]);
|
|
|
}
|
|
|
- } else {
|
|
|
- // 如果 episode_number 是单个值,直接插入
|
|
|
- $records[] = [
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ dLog('anime')->error('创建资产图片生成任务失败', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 标记为失败
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_status' => '生成失败',
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (empty($taskIds)) {
|
|
|
+ Utils::throwError('20003:没有成功创建任何图片生成任务');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 返回 SSE 流
|
|
|
+ return $this->pollProductImageTasks($script_id, $script_name, $productTaskMap, $type_folder_ids);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 轮询资产图片生成任务状态(SSE流式返回)
|
|
|
+ * 只通过查询数据库获取任务状态,不主动调用API
|
|
|
+ *
|
|
|
+ * @param int $script_id 剧本ID
|
|
|
+ * @param string $script_name 剧本名称
|
|
|
+ * @param array $productTaskMap 资产ID到任务ID的映射
|
|
|
+ * @param array $type_folder_ids 类型文件夹ID映射
|
|
|
+ * @return \Generator
|
|
|
+ */
|
|
|
+ private function pollProductImageTasks($script_id, $script_name, $productTaskMap, $type_folder_ids) {
|
|
|
+ $startTime = time();
|
|
|
+ $timeout = 1800; // 30分钟超时
|
|
|
+ $pollInterval = 10; // 10秒轮询一次
|
|
|
+ $lastStatus = []; // 记录上次的状态,用于判断是否有变化
|
|
|
+
|
|
|
+ // 定义类型名称映射
|
|
|
+ $typeNames = [
|
|
|
+ 1 => 'roles', // 角色
|
|
|
+ 2 => 'scenes', // 场景
|
|
|
+ 3 => 'props' // 道具
|
|
|
+ ];
|
|
|
+
|
|
|
+ while (time() - $startTime < $timeout) {
|
|
|
+ sleep($pollInterval);
|
|
|
+
|
|
|
+ // 查询所有资产的当前状态
|
|
|
+ $products = DB::table('mp_products')
|
|
|
+ ->whereIn('id', array_keys($productTaskMap))
|
|
|
+ ->where('is_deleted', 0)
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ // 统计各类型的状态(使用类型名称作为键名)
|
|
|
+ $stats = [
|
|
|
+ 'character' => ['total' => 0, 'success' => 0, 'completed' => 0], // 角色
|
|
|
+ 'scene' => ['total' => 0, 'success' => 0, 'completed' => 0], // 场景
|
|
|
+ 'prop' => ['total' => 0, 'success' => 0, 'completed' => 0], // 道具
|
|
|
+ ];
|
|
|
+
|
|
|
+ $allCompleted = true;
|
|
|
+ $hasStatusChange = false;
|
|
|
+
|
|
|
+ foreach ($products as $product) {
|
|
|
+ $type = $product->product;
|
|
|
+ $typeName = $typeNames[$type] ?? 'unknown'; // 使用 unknown 作为未知类型
|
|
|
+
|
|
|
+ // 如果类型名称不在 stats 中,初始化
|
|
|
+ if (!isset($stats[$typeName])) {
|
|
|
+ $stats[$typeName] = ['total' => 0, 'success' => 0, 'completed' => 0];
|
|
|
+ }
|
|
|
+
|
|
|
+ $stats[$typeName]['total']++;
|
|
|
+
|
|
|
+ $task_id = $product->pic_task_id;
|
|
|
+ $currentStatus = $product->pic_task_status;
|
|
|
+
|
|
|
+ // 检查状态是否有变化
|
|
|
+ if (!isset($lastStatus[$product->id]) || $lastStatus[$product->id] !== $currentStatus) {
|
|
|
+ $hasStatusChange = true;
|
|
|
+ $lastStatus[$product->id] = $currentStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是生成中,查询任务表状态
|
|
|
+ if ($currentStatus === '生成中' && $task_id) {
|
|
|
+ // 只查询数据库,不调用API
|
|
|
+ $task = DB::table('mp_generate_pic_tasks')
|
|
|
+ ->where('id', $task_id)
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if ($task) {
|
|
|
+ // 检查任务状态
|
|
|
+ if ($task->status === 'success' && $task->result_url) {
|
|
|
+ // 解析图片URL
|
|
|
+ $result_urls = $task->result_url;
|
|
|
+ if (is_string($result_urls)) {
|
|
|
+ $result_urls = json_decode($result_urls, true);
|
|
|
+ }
|
|
|
+ $img_url = is_array($result_urls) ? $result_urls[0] : $task->result_url;
|
|
|
+
|
|
|
+ // 更新资产表状态
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_status' => '生成成功',
|
|
|
+ 'url' => $img_url,
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $stats[$typeName]['success']++;
|
|
|
+ $stats[$typeName]['completed']++;
|
|
|
+ $hasStatusChange = true;
|
|
|
+
|
|
|
+ dLog('anime')->info('资产图片生成成功', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'task_id' => $task_id,
|
|
|
+ 'img_url' => $img_url
|
|
|
+ ]);
|
|
|
+ } elseif ($task->status === 'failed') {
|
|
|
+ // 更新资产表状态
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_status' => '生成失败',
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $stats[$typeName]['completed']++;
|
|
|
+ $hasStatusChange = true;
|
|
|
+
|
|
|
+ dLog('anime')->warning('资产图片生成失败', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'task_id' => $task_id,
|
|
|
+ 'error' => $task->error_message ?? '未知错误'
|
|
|
+ ]);
|
|
|
+ } else {
|
|
|
+ // 任务还在处理中
|
|
|
+ $allCompleted = false;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 任务不存在,标记为失败
|
|
|
+ DB::table('mp_products')
|
|
|
+ ->where('id', $product->id)
|
|
|
+ ->update([
|
|
|
+ 'pic_task_status' => '生成失败',
|
|
|
+ 'updated_at' => now()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $stats[$type]['completed']++;
|
|
|
+ $hasStatusChange = true;
|
|
|
+
|
|
|
+ dLog('anime')->error('图片生成任务不存在', [
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'task_id' => $task_id
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ } elseif ($currentStatus === '生成成功') {
|
|
|
+ $stats[$typeName]['success']++;
|
|
|
+ $stats[$typeName]['completed']++;
|
|
|
+ } elseif ($currentStatus === '生成失败') {
|
|
|
+ $stats[$typeName]['completed']++;
|
|
|
+ } else {
|
|
|
+ // 其他状态也认为未完成
|
|
|
+ $allCompleted = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除没有数据的类型(total=0)
|
|
|
+ foreach ($stats as $typeName => $stat) {
|
|
|
+ if ($stat['total'] === 0) {
|
|
|
+ unset($stats[$typeName]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有状态变化,发送 SSE 更新
|
|
|
+ if ($hasStatusChange) {
|
|
|
+ $eventType = $allCompleted ? 'complete' : 'update';
|
|
|
+
|
|
|
+ $response = [
|
|
|
+ 'type' => $eventType,
|
|
|
'script_id' => $script_id,
|
|
|
- 'product_id' => $productId,
|
|
|
- 'episode_number' => $episodeNumbers,
|
|
|
- 'created_at' => $now,
|
|
|
- 'updated_at' => $now,
|
|
|
+ 'script_name' => $script_name,
|
|
|
+ 'stats' => $stats,
|
|
|
+ 'timestamp' => date('Y-m-d H:i:s')
|
|
|
];
|
|
|
+
|
|
|
+ yield "data: " . json_encode($response, JSON_UNESCAPED_UNICODE) . "\n\n";
|
|
|
+
|
|
|
+ dLog('anime')->info('SSE更新', [
|
|
|
+ 'event_type' => $eventType,
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'script_name' => $script_name,
|
|
|
+ 'stats' => $stats
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if ($allCompleted) {
|
|
|
+ dLog('anime')->info('所有资产图片生成任务已完成', [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'script_name' => $script_name,
|
|
|
+ 'stats' => $stats
|
|
|
+ ]);
|
|
|
+ break;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- if (empty($records)) {
|
|
|
- Utils::throwError('20003:没有有效的产品数据');
|
|
|
+
|
|
|
+ // 超时处理
|
|
|
+ if (time() - $startTime >= $timeout && !$allCompleted) {
|
|
|
+ $response = [
|
|
|
+ 'type' => 'timeout',
|
|
|
+ 'message' => '部分任务超时,请稍后查看结果',
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'script_name' => $script_name,
|
|
|
+ 'timestamp' => date('Y-m-d H:i:s')
|
|
|
+ ];
|
|
|
+
|
|
|
+ yield "data: " . json_encode($response, JSON_UNESCAPED_UNICODE) . "\n\n";
|
|
|
+
|
|
|
+ dLog('anime')->warning('资产图片生成任务超时', [
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'script_name' => $script_name,
|
|
|
+ 'timeout' => $timeout
|
|
|
+ ]);
|
|
|
}
|
|
|
-
|
|
|
- // 批量插入数据(使用 insertOrIgnore 避免重复插入)
|
|
|
- return DB::table('mp_script_product_mappings')->insertOrIgnore($records);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -6469,7 +7149,6 @@ class AnimeService
|
|
|
->join('mp_products as product', 'mapping.product_id', '=', 'product.id')
|
|
|
->where('mapping.script_id', $script_id)
|
|
|
->where('product.cpid', $cpid)
|
|
|
- // ->where('product.user_id', $uid)
|
|
|
->where('product.is_deleted', 0);
|
|
|
|
|
|
// 如果提供了 episode_number,则过滤指定集数
|
|
|
@@ -6477,37 +7156,93 @@ class AnimeService
|
|
|
$query->where('mapping.episode_number', $episode_number);
|
|
|
}
|
|
|
|
|
|
- $products = $query->select(
|
|
|
+ $mappings = $query->select(
|
|
|
'mapping.script_id',
|
|
|
'mapping.product_id',
|
|
|
'mapping.episode_number',
|
|
|
'product.product_name',
|
|
|
+ 'product.type',
|
|
|
'product.product',
|
|
|
'product.pic_prompt',
|
|
|
'product.url',
|
|
|
+ 'product.pic_task_id',
|
|
|
+ 'product.pic_task_status',
|
|
|
'product.three_view_image_url',
|
|
|
'mapping.created_at'
|
|
|
)
|
|
|
+ ->orderBy('mapping.product_id', 'asc')
|
|
|
->orderBy('mapping.episode_number', 'asc')
|
|
|
- ->orderBy('mapping.id', 'asc')
|
|
|
- ->get()
|
|
|
- ->map(function ($item) {
|
|
|
- return [
|
|
|
- 'script_id' => $item->script_id,
|
|
|
- 'episode_number' => $item->episode_number,
|
|
|
- 'product_id' => $item->product_id,
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ // 按 product_id 分组,合并 episode_number
|
|
|
+ $productMap = [];
|
|
|
+ foreach ($mappings as $item) {
|
|
|
+ $product_id = $item->product_id;
|
|
|
+
|
|
|
+ if (!isset($productMap[$product_id])) {
|
|
|
+ $productMap[$product_id] = [
|
|
|
+ 'product_id' => $product_id,
|
|
|
'product_name' => $item->product_name,
|
|
|
+ 'type' => (int)$item->type,
|
|
|
'product' => (int)$item->product,
|
|
|
'pic_prompt' => $item->pic_prompt,
|
|
|
'url' => $item->url,
|
|
|
- // 'three_view_image_url' => $item->three_view_image_url,
|
|
|
+ 'pic_task_id' => $item->pic_task_id,
|
|
|
+ 'pic_task_status' => $item->pic_task_status,
|
|
|
+ 'episode_numbers' => [],
|
|
|
'created_at' => $item->created_at,
|
|
|
];
|
|
|
- })
|
|
|
- ->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加 episode_number(去重)
|
|
|
+ if (!in_array($item->episode_number, $productMap[$product_id]['episode_numbers'])) {
|
|
|
+ $productMap[$product_id]['episode_numbers'][] = $item->episode_number;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按类型分组
|
|
|
+ $roles = []; // type=1 角色/主体
|
|
|
+ $scenes = []; // type=2 场景
|
|
|
+ $props = []; // type=3 道具
|
|
|
+
|
|
|
+ foreach ($productMap as $product) {
|
|
|
+ // 将 episode_numbers 数组转换为逗号分隔的字符串
|
|
|
+ $episodeNumbersStr = implode(',', $product['episode_numbers']);
|
|
|
+
|
|
|
+ // 构造返回数据
|
|
|
+ $productData = [
|
|
|
+ 'product_id' => $product['product_id'],
|
|
|
+ 'product_name' => $product['product_name'],
|
|
|
+ 'product' => $product['product'],
|
|
|
+ 'pic_prompt' => $product['pic_prompt'],
|
|
|
+ 'url' => $product['url'],
|
|
|
+ 'pic_task_id' => $product['pic_task_id'],
|
|
|
+ 'pic_task_status' => $product['pic_task_status'],
|
|
|
+ 'episode_numbers' => $episodeNumbersStr,
|
|
|
+ 'created_at' => $product['created_at'],
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 根据 type 分类(注意:type 字段表示资产类型,不是文件夹)
|
|
|
+ // 根据 product 字段分类(1=角色, 2=场景, 3=道具)
|
|
|
+ switch ($product['product']) {
|
|
|
+ case 1:
|
|
|
+ $roles[] = $productData;
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ $scenes[] = $productData;
|
|
|
+ break;
|
|
|
+ case 3:
|
|
|
+ $props[] = $productData;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
return [
|
|
|
- 'list' => $products,
|
|
|
+ 'script_id' => $script_id,
|
|
|
+ 'script_name' => $script->script_name ?? '',
|
|
|
+ 'roles' => $roles, // 主体
|
|
|
+ 'scenes' => $scenes, // 场景
|
|
|
+ 'props' => $props, // 道具
|
|
|
];
|
|
|
}
|
|
|
|