get("img_to_img_task:{$taskId}"); if ($existing) { $info = json_decode($existing, true); if (isset($info['status']) && $info['status'] === 'completed') { echo "任务 {$taskId} 已完成,跳过重复执行\n"; $job->delete(); return; } } try { echo " 开始处理图生图".date('Y-m-d H:i:s')."\n"; $result = $this->get_img_to_img($data); // get_img_to_img 内部已写入 img_to_img_task,此处无需重复写入 if (is_array($result) && isset($result['code']) && $result['code'] !== 0) { throw new \Exception($result['msg'] ?? '图生图失败'); } echo "🎉 任务 {$taskId} 执行完成,图片生成成功!\n"; echo "结束时间:" . date('Y-m-d H:i:s') . "\n"; $job->delete(); } catch (\Exception $e) { echo "图生图失败: " . $e->getMessage() . "\n"; $job->delete(); } $job->delete(); } else { $logId = $data['log_id'] ?? null; try { // 任务类型校验(必须是图生图) if (!isset($data['type']) || $data['type'] !== '图生图') { $job->delete(); return; } $startTime = date('Y-m-d H:i:s'); echo "━━━━━━━━━━ ▶ 图生图任务开始处理━━━━━━━━━━\n"; echo "处理时间:{$startTime}\n"; // 更新日志状态:处理中 if ($logId) { Db::name('image_task_log')->where('id', $logId)->update([ 'status' => 1, 'log' => '图生图处理中', 'update_time' => $startTime ]); } //拼接原图文件路径 + 图片名称 $old_image_url = rtrim($data['sourceDir'], '/') . '/' . ltrim($data['file_name'], '/'); $list = Db::name("text_to_image") ->where('old_image_url', $old_image_url) ->where('img_name', '<>', '') // ->where('status', 1) ->select(); if (!empty($list)) { $total = count($list); echo "📊 共需处理:{$total} 条记录\n\n"; foreach ($list as $index => $row) { $currentIndex = $index + 1; $begin = date('Y-m-d H:i:s'); echo "处理时间:{$begin}\n"; echo "👉 正在处理第 {$currentIndex} 条,ID: {$row['id']}\n"; // 调用生成图像方法 $result = $this->ImageToImage( $data["file_name"], $data["outputDir"], $row["new_image_url"], $row["img_name"], 1024, 1303 ); $resultText = ($result === true || $result === 1 || $result === '成功') ? '成功' : '失败或无返回'; echo "✅ 处理结果:{$resultText}\n"; $end = date('Y-m-d H:i:s'); echo "完成时间:{$end}\n"; echo "Processed: " . static::class . "\n"; echo "图生图已处理完成\n\n"; } // 更新日志状态:成功 if ($logId) { Db::name('image_task_log')->where('id', $logId)->update([ 'status' => 2, 'log' => '图生图处理成功', 'update_time' => date('Y-m-d H:i:s') ]); } echo date('Y-m-d H:i:s') . " 图生图任务全部完成\n"; } else { echo "未找到可处理的数据,跳过执行\n"; if ($logId) { Db::name('image_task_log')->where('id', $logId)->update([ 'status' => 2, 'log' => '无数据可处理,已跳过'.$old_image_url, 'update_time' => date('Y-m-d H:i:s') ]); } } // 如果还有链式任务,继续推送 if (!empty($data['chain_next'])) { $nextType = array_shift($data['chain_next']); $data['type'] = $nextType; Queue::push('app\job\ImageArrJob', [ 'task_id' => $data['task_id'], 'data' => [$data] ], 'arrimage'); } $job->delete(); } catch (\Exception $e) { //异常处理,记录失败日志 echo "错误信息: " . $e->getMessage() . "\n"; echo "文件: " . $e->getFile() . "\n"; echo "行号: " . $e->getLine() . "\n"; // 删除当前任务 $job->delete(); } } } /** * 失败回调(可用于后续通知或重试机制) */ public function failed($data) { echo "ImageJob failed: " . json_encode($data); } /** * 图生图主处理方法:根据产品图+参考图生成新图 * * 业务分支: * - ProductImageGeneration:产品图创作(前端传 base64,先调模型成功后再落盘入库) * - ProductTemplateReplace:产品替换(读取已有产品图/模板图路径,生成效果图并返回前端展示) * * 入参说明($data): * - prompt: 提示词 * - size: 尺寸(如 1:1、9:16、768x1024) * - status_val: 任务类型(图生图) * - status_type: 页面/流程类型(决定走哪个业务分支) * - product_img: 产品图(base64 或相对路径) * - template_img: 参考图(base64 或相对路径) * - model: 模型名称 * - sys_id: 操作用户 * - task_id: 可选,异步任务ID(用于写 Redis 状态) * * @param array $data 图生图任务参数 * @return string|array 成功返回生成图相对路径;失败返回 ['code'=>1,'msg'=>错误信息] */ public function get_img_to_img($data) { // 1) 基础参数解析 $prompt = trim($data['prompt'] ?? ''); $size = trim($data['size'] ?? ''); $statusVal = trim($data['status_val'] ?? ''); $statusType = trim($data['status_type'] ?? ''); $productImg = trim($data['product_img'] ?? ''); $templateImg = trim($data['template_img'] ?? ''); $oldImg = trim($data['old_img'] ?? ''); $refImg = trim($data['ref_img'] ?? ''); $height = trim((string)($data['height'] ?? '')); $width = trim((string)($data['width'] ?? '')); $model = trim($data['model'] ?? ''); $sysId = (int)($data['sys_id'] ?? 0); $now = date('Y-m-d H:i:s'); // texttoimage:前端传 old_img(base64/路径),size 为空时用 width x height if ($statusType === 'texttoimage') { if ($size === '' && $width !== '' && $height !== '') { $size = $width . 'x' . $height; } if ($size === '') { $size = '1:1'; } } // 失败统一回写任务状态,避免多处重复代码 $fail = function ($msg) use ($data) { if (!empty($data['task_id'])) { try { $redis = getTaskRedis(); $redis->set("img_to_img_task:" . $data['task_id'], json_encode([ 'status' => 'failed', 'msg' => $msg, 'error' => $msg, 'completed_at' => date('Y-m-d H:i:s') ], JSON_UNESCAPED_UNICODE), ['EX' => 300]); } catch (\Exception $e) { // Redis 不可用时不阻断主流程 } } return ['code' => 1, 'msg' => $msg]; }; // 成功统一回写任务状态 $complete = function ($imgPath) use ($data) { if (!empty($data['task_id'])) { try { $redis = getTaskRedis(); $redis->set("img_to_img_task:" . $data['task_id'], json_encode([ 'status' => 'completed', 'image' => $imgPath, 'image_url' => $imgPath, 'completed_at' => date('Y-m-d H:i:s') ], JSON_UNESCAPED_UNICODE), ['EX' => 300]); } catch (\Exception $e) { // Redis 不可用时不阻断主流程 } } }; // 2) 解析输入图片(按页面类型分支) $productBase64 = null; $productMime = 'image/png'; $templateBase64 = null; $templateMime = 'image/png'; $productExt = 'png'; $templateExt = 'png'; $sourceProductPath = ''; $sourceTemplatePath = ''; if ($statusType === 'ProductImageGeneration') { // 产品图创作:前端直接传 base64,先解析,等模型成功后再入库/落盘 preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $productImg, $pm); if (empty($pm)) { return $fail('产品图未找到图片数据'); } $productBase64 = preg_replace('/\s+/', '', $pm[2]); $productMime = ($pm[1] === 'jpg' ? 'image/jpeg' : 'image/' . $pm[1]); $productExt = $pm[1]; preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $templateImg, $tm); if (empty($tm)) { return $fail('模板图未找到图片数据'); } $templateBase64 = preg_replace('/\s+/', '', $tm[2]); $templateMime = ($tm[1] === 'jpg' ? 'image/jpeg' : 'image/' . $tm[1]); $templateExt = $tm[1]; } elseif ($statusType === 'ProductTemplateReplace') { // 产品替换:优先转为 OSS 完整 URL 再读取,兼容库中存相对路径 $productImgSource = Common::ossFullUrl((string)$productImg); $templateImgSource = Common::ossFullUrl((string)$templateImg); try { $productImgRaw = AIGatewayService::file_get_contents($productImgSource); $productBase64 = $productImgRaw['base64Data']; $productMime = $productImgRaw['mimeType']; $templateImgRaw = AIGatewayService::file_get_contents($templateImgSource); $templateBase64 = $templateImgRaw['base64Data']; $templateMime = $templateImgRaw['mimeType']; } catch (\Exception $e) { // 回传清晰错误,方便定位是产品图还是模板图读取失败 return $fail('读取产品图/模板图失败: ' . $e->getMessage()); } } elseif ($statusType === 'texttoimage') { // 前端传 old_img(原图 base64 或路径),ref_img(参考图,可选) $inputOldImg = $oldImg !== '' ? $oldImg : $productImg; $inputRefImg = $refImg !== '' ? $refImg : $templateImg; if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,(.+)$/is', $inputOldImg, $pm)) { $productBase64 = preg_replace('/\s+/', '', $pm[2]); $ext = strtolower($pm[1]); $productMime = ($ext === 'jpg' ? 'image/jpeg' : 'image/' . $ext); $productExt = ($ext === 'jpeg' ? 'jpg' : $ext); } elseif ($inputOldImg !== '') { $sourceProductPath = ltrim(str_replace('\\', '/', $inputOldImg), '/'); try { $productImgSource = Common::ossFullUrl((string)$inputOldImg); $productImgRaw = AIGatewayService::file_get_contents($productImgSource); $productBase64 = $productImgRaw['base64Data']; $productMime = $productImgRaw['mimeType']; } catch (\Exception $e) { return $fail('读取原图失败: ' . $e->getMessage()); } } else { return $fail('原图不能为空'); } if ($inputRefImg !== '') { if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,(.+)$/is', $inputRefImg, $tm)) { $templateBase64 = preg_replace('/\s+/', '', $tm[2]); $ext = strtolower($tm[1]); $templateMime = ($ext === 'jpg' ? 'image/jpeg' : 'image/' . $ext); $templateExt = ($ext === 'jpeg' ? 'jpg' : $ext); } else { $sourceTemplatePath = ltrim(str_replace('\\', '/', $inputRefImg), '/'); try { $templateImgSource = Common::ossFullUrl((string)$inputRefImg); $templateImgRaw = AIGatewayService::file_get_contents($templateImgSource); $templateBase64 = $templateImgRaw['base64Data']; $templateMime = $templateImgRaw['mimeType']; } catch (\Exception $e) { return $fail('读取参考图失败: ' . $e->getMessage()); } } } } else { return $fail('当前页面未进行配置,请联系管理员开通权限'); } // 3) 调模型生成图像 if ($statusType === 'texttoimage') { $promptContent = $prompt !== '' ? $prompt : '请根据原图生成高质量图片'; // 原图/参考图先落盘并上传 OSS(即使 AI 失败也能在 OSS 看到 texttoimage 目录) $rootPath = str_replace('\\', '/', ROOT_PATH); $dateSegment = date('Y-m-d'); $saveDir = rtrim($rootPath, '/') . '/public/uploads/texttoimage/' . $dateSegment . '/'; if (!is_dir($saveDir)) { mkdir($saveDir, 0755, true); } if ($sourceProductPath === '' && !empty($productBase64)) { $sourceFile = 'source-' . uniqid() . '.' . $productExt; $sourceImageData = base64_decode($productBase64); if ($sourceImageData === false || !file_put_contents($saveDir . $sourceFile, $sourceImageData)) { return $fail('原图保存失败'); } $sourceProductPath = 'uploads/texttoimage/' . $dateSegment . '/' . $sourceFile; if (!Common::uploadLocalFileToOss((string)($saveDir . $sourceFile), (string)$sourceProductPath)) { echo 'OSS 上传原图失败: ' . $sourceProductPath . "\n"; } } if ($sourceTemplatePath === '' && !empty($templateBase64)) { $refFile = 'ref-' . uniqid() . '.' . $templateExt; $refImageData = base64_decode($templateBase64); if ($refImageData !== false && file_put_contents($saveDir . $refFile, $refImageData)) { $sourceTemplatePath = 'uploads/texttoimage/' . $dateSegment . '/' . $refFile; if (!Common::uploadLocalFileToOss((string)($saveDir . $refFile), (string)$sourceTemplatePath)) { echo 'OSS 上传参考图失败: ' . $sourceTemplatePath . "\n"; } } } } else { $defaultPrompt = '请完成产品模板替换: 1. 从产品图提取产品主体、品牌名称、核心文案; 2. 从模板图继承版式布局、文字排版、色彩风格、背景元素; 3. 将模板图中的产品和文字替换为产品图的内容; 4. 最终生成的图片与模板图视觉风格统一,仅替换产品和文字。'; $promptContent = $prompt ? $prompt . "\n\n" . $defaultPrompt : $defaultPrompt; } $aiGateway = new AIGatewayService(); // texttoimage:原图 + 提示词走图生图(gemini-3-pro-image-preview) $res = $aiGateway->buildRequestData( $statusVal, $model, $promptContent, $size, (string)$productBase64, $productMime, (string)$templateBase64, $templateMime ); $generatedBase64 = $aiGateway->extractImageBase64FromResponse($res); if (!$generatedBase64) { $aiGateway->logImageResponseDebug($res, $data['task_id'] ?? ''); return $fail($aiGateway->describeImageExtractFailure($res)); } $generatedImageData = base64_decode($generatedBase64); if ($generatedImageData === false || strlen($generatedImageData) < 100) { return $fail('图片Base64解码失败'); } // 4) 按业务分支落盘入库 if ($statusType === 'ProductImageGeneration') { // 4.1 产品图创作:保存产品图/模板图/生成图 -> 插入 product_image_generate $rootPath = str_replace('\\', '/', ROOT_PATH); $saveDir = rtrim($rootPath, '/') . '/public/uploads/Product/' . date('Y-m-d') . '/'; if (!is_dir($saveDir)) { mkdir($saveDir, 0755, true); } $productFile = 'product-' . uniqid() . '.' . $productExt; $productImageData = base64_decode($productBase64); if ($productImageData === false || !file_put_contents($saveDir . $productFile, $productImageData)) { return $fail('产品图保存失败'); } $productDbPath = 'uploads/Product/' . date('Y-m-d') . '/' . $productFile; Common::uploadLocalFileToOss((string)($saveDir . $productFile), (string)$productDbPath); $templateFile = 'template-' . uniqid() . '.' . $templateExt; $templateImageData = base64_decode($templateBase64); if ($templateImageData === false || !file_put_contents($saveDir . $templateFile, $templateImageData)) { return $fail('模板图保存失败'); } $templateDbPath = 'uploads/Product/' . date('Y-m-d') . '/' . $templateFile; Common::uploadLocalFileToOss((string)($saveDir . $templateFile), (string)$templateDbPath); $fileName = uniqid() . '.png'; if (!file_put_contents($saveDir . $fileName, $generatedImageData)) { return $fail('生成图保存失败'); } $generatedDbPath = 'uploads/Product/' . date('Y-m-d') . '/' . $fileName; Common::uploadLocalFileToOss((string)($saveDir . $fileName), (string)$generatedDbPath); Db::name('product_image_generate')->insert([ 'prompt' => $prompt, 'model' => $model, 'product_img' => $productDbPath, 'reference_image' => $templateDbPath, 'generated_image' => $generatedDbPath, 'status_val' => $statusVal, 'size' => $size, 'sys_id' => $sysId, 'createTime' => $now, ]); $complete($generatedDbPath); return $generatedDbPath; } else if ($statusType === 'ProductTemplateReplace') { // 4.2 产品替换:生成图落盘到 merchant/newimg -> 回写 product + product_image $product = Db::name('product')->where('id', $data['id'])->find(); if (empty($product)) { return $fail('产品不存在'); } $productCode = $product['product_code']; $productPrefix = substr($productCode, 0, 9); $rootPath = str_replace('\\', '/', ROOT_PATH); $saveDir = rtrim($rootPath, '/') . '/public/uploads/merchant/' . $productPrefix . '/' . $productCode . '/newimg/'; if (!is_dir($saveDir)) { mkdir($saveDir, 0755, true); } $fileName = 'img2img-' . date('YmdHis') . '-' . uniqid() . '.png'; $fullPath = $saveDir . $fileName; if (!file_put_contents($fullPath, $generatedImageData)) { return $fail('图片保存失败'); } $dbImgPath = 'uploads/merchant/' . $productPrefix . '/' . $productCode . '/newimg/' . $fileName; Common::uploadLocalFileToOss((string)$fullPath, (string)$dbImgPath); Db::name('product')->where('id', $data['id'])->update([ 'createTime' => date('Y-m-d H:i:s'), 'content' => $data['prompt'], 'product_new_img' => $dbImgPath ]); Db::name('product_image')->insert([ 'product_id' => $data['id'], 'product_new_img' => $dbImgPath, 'product_content' => $data['prompt'], 'template_id' => $data['template_id'], 'createTime' => date('Y-m-d H:i:s'), ]); $complete($dbImgPath); return $dbImgPath; } elseif ($statusType === 'texttoimage') { $rootPath = str_replace('\\', '/', ROOT_PATH); $dateSegment = date('Y-m-d'); $saveDir = rtrim($rootPath, '/') . '/public/uploads/texttoimage/' . $dateSegment . '/'; if (!is_dir($saveDir)) { mkdir($saveDir, 0755, true); } $fileName = 'text2img-' . date('YmdHis') . '-' . uniqid() . '.png'; $fullPath = $saveDir . $fileName; if (!file_put_contents($fullPath, $generatedImageData)) { return $fail('图片保存失败'); } $dbImgPath = 'uploads/texttoimage/' . $dateSegment . '/' . $fileName; if (!Common::uploadLocalFileToOss((string)$fullPath, (string)$dbImgPath)) { echo 'OSS 上传生成图失败: ' . $dbImgPath . "\n"; } // size 字段为 double:优先存 width,其次 size 数值 $sizeForDb = 0; if ($width !== '' && is_numeric($width)) { $sizeForDb = (float)$width; } elseif ($size !== '' && is_numeric($size)) { $sizeForDb = (float)$size; } Db::name('ai_text_image')->insert([ 'old_img' => $sourceProductPath, 'ref_img' => $sourceTemplatePath, 'new_img' => $dbImgPath, 'prompt' => $prompt, 'size' => $sizeForDb, 'sys_id' => $sysId, 'sys_rq' => $now, ]); $complete($dbImgPath); return $dbImgPath; } else { return $fail('当前页面未进行配置,请联系管理员开通权限'); } } public function ImageToImage($fileName, $outputDirRaw, $new_image_url, $width, $height) { $rootPath = str_replace('\\', '/', ROOT_PATH); $outputDir = rtrim($rootPath . 'public/' . ltrim($outputDirRaw, '/'), '/') . '/'; $dateDir = date('Y-m-d') . '/'; $fullBaseDir = $outputDir . $dateDir; // 创建主目录和 imgtoimg 子目录 if (!is_dir($fullBaseDir)) { mkdir($fullBaseDir, 0755, true); } $imgtoimgDir = $fullBaseDir . '1024x1303/'; if (!is_dir($imgtoimgDir)) { mkdir($imgtoimgDir, 0755, true); } // 查询数据库原图记录 $record = Db::name('text_to_image') ->where('old_image_url', 'like', "%{$fileName}") ->order('id desc') ->find(); if (!$record) { return json(['code' => 1, 'msg' => '没有找到匹配的图像记录']); } // 调用 AI 图生图 API $ai = new AIGatewayService(); $res = $ai->txt2imgWithControlNet('', $new_image_url); if (!isset($res['code']) || $res['code'] !== 0) { return json(['code' => 1, 'msg' => $res['msg'] ?? '图像生成失败']); } // 生成保存文件路径 $originalBaseName = pathinfo($new_image_url, PATHINFO_FILENAME); $finalFileName = $originalBaseName . '.png'; $savePath = $imgtoimgDir . $finalFileName; // 写入图像文件 if (!file_put_contents($savePath, base64_decode($res['data']['base64']))) { return json(['code' => 1, 'msg' => '图像保存失败,请检查目录权限']); } // 图生图结果同步 OSS(失败不阻断) $relativeImgPath = rtrim($outputDirRaw, '/') . '/' . $dateDir . '1024x1303/' . $finalFileName; Common::uploadLocalFileToOss((string)$savePath, (string)$relativeImgPath); // 构造相对路径用于数据库 // 更新数据库记录 Db::name('text_to_image')->where('id', $record['id'])->update([ 'imgtoimg_url' => $relativeImgPath, 'status_name' => '图生图', 'error_msg' => '', 'update_time' => date('Y-m-d H:i:s') ]); // 返回成功响应 return "成功"; } }