ImageToImageJob.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <?php
  2. namespace app\job;
  3. use app\api\controller\Common;
  4. use app\service\AIGatewayService;
  5. use app\service\ImageService;
  6. use think\Db;
  7. use think\queue\Job;
  8. use think\Queue;
  9. //图生图
  10. class ImageToImageJob{
  11. public function fire(Job $job, $data)
  12. {
  13. //产品图+模板图) 时走此分支
  14. if (isset($data['status_val']) && $data['status_val'] == '图生图') {
  15. $taskId = $data['task_id'];
  16. // 幂等:若任务已完成则跳过,避免超时重试导致重复执行
  17. $redis = getTaskRedis();
  18. $existing = $redis->get("img_to_img_task:{$taskId}");
  19. if ($existing) {
  20. $info = json_decode($existing, true);
  21. if (isset($info['status']) && ImageService::isTaskCompleted($info['status'])) {
  22. echo "任务 {$taskId} 已完成,跳过重复执行\n";
  23. $job->delete();
  24. return;
  25. }
  26. }
  27. try {
  28. // 标记处理中(前端轮询可见)
  29. $redis->set("img_to_img_task:{$taskId}", json_encode([
  30. 'status' => ImageService::TASK_STATUS_PROCESSING,
  31. 'started_at' => date('Y-m-d H:i:s'),
  32. ], JSON_UNESCAPED_UNICODE), ['EX' => 300]);
  33. \think\Log::info("[ImageToImageJob] start task_id={$taskId} status_type=" . ($data['status_type'] ?? ''));
  34. $result = $this->get_img_to_img($data);
  35. // get_img_to_img 内部已写入 img_to_img_task,此处无需重复写入
  36. if (is_array($result) && isset($result['code']) && $result['code'] !== 0) {
  37. throw new \Exception($result['msg'] ?? '图生图失败');
  38. }
  39. echo "🎉 任务 {$taskId} 执行完成,图片生成成功!\n";
  40. echo "结束时间:" . date('Y-m-d H:i:s') . "\n";
  41. $job->delete();
  42. } catch (\Exception $e) {
  43. echo "图生图失败: " . $e->getMessage() . "\n";
  44. \think\Log::error('[ImageToImageJob] ' . $e->getMessage());
  45. $job->delete();
  46. }
  47. } else {
  48. $logId = $data['log_id'] ?? null;
  49. try {
  50. // 任务类型校验(必须是图生图)
  51. if (!isset($data['type']) || $data['type'] !== '图生图') {
  52. $job->delete();
  53. return;
  54. }
  55. $startTime = date('Y-m-d H:i:s');
  56. echo "━━━━━━━━━━ ▶ 图生图任务开始处理━━━━━━━━━━\n";
  57. echo "处理时间:{$startTime}\n";
  58. // 更新日志状态:处理中
  59. if ($logId) {
  60. Db::name('image_task_log')->where('id', $logId)->update([
  61. 'status' => 1,
  62. 'log' => '图生图处理中',
  63. 'update_time' => $startTime
  64. ]);
  65. }
  66. //拼接原图文件路径 + 图片名称
  67. $old_image_url = rtrim($data['sourceDir'], '/') . '/' . ltrim($data['file_name'], '/');
  68. $list = Db::name("text_to_image")
  69. ->where('old_image_url', $old_image_url)
  70. ->where('img_name', '<>', '')
  71. // ->where('status', 1)
  72. ->select();
  73. if (!empty($list)) {
  74. $total = count($list);
  75. echo "📊 共需处理:{$total} 条记录\n\n";
  76. foreach ($list as $index => $row) {
  77. $currentIndex = $index + 1;
  78. $begin = date('Y-m-d H:i:s');
  79. echo "处理时间:{$begin}\n";
  80. echo "👉 正在处理第 {$currentIndex} 条,ID: {$row['id']}\n";
  81. // 调用生成图像方法
  82. $result = $this->ImageToImage(
  83. $data["file_name"],
  84. $data["outputDir"],
  85. $row["new_image_url"],
  86. $row["img_name"],
  87. 1024,
  88. 1303
  89. );
  90. $resultText = ($result === true || $result === 1 || $result === '成功') ? '成功' : '失败或无返回';
  91. echo "✅ 处理结果:{$resultText}\n";
  92. $end = date('Y-m-d H:i:s');
  93. echo "完成时间:{$end}\n";
  94. echo "Processed: " . static::class . "\n";
  95. echo "图生图已处理完成\n\n";
  96. }
  97. // 更新日志状态:成功
  98. if ($logId) {
  99. Db::name('image_task_log')->where('id', $logId)->update([
  100. 'status' => 2,
  101. 'log' => '图生图处理成功',
  102. 'update_time' => date('Y-m-d H:i:s')
  103. ]);
  104. }
  105. echo date('Y-m-d H:i:s') . " 图生图任务全部完成\n";
  106. } else {
  107. echo "未找到可处理的数据,跳过执行\n";
  108. if ($logId) {
  109. Db::name('image_task_log')->where('id', $logId)->update([
  110. 'status' => 2,
  111. 'log' => '无数据可处理,已跳过'.$old_image_url,
  112. 'update_time' => date('Y-m-d H:i:s')
  113. ]);
  114. }
  115. }
  116. // 如果还有链式任务,继续推送
  117. if (!empty($data['chain_next'])) {
  118. $nextType = array_shift($data['chain_next']);
  119. $data['type'] = $nextType;
  120. Queue::push('app\job\ImageArrJob', [
  121. 'task_id' => $data['task_id'],
  122. 'data' => [$data]
  123. ], 'arrimage');
  124. }
  125. $job->delete();
  126. } catch (\Exception $e) {
  127. //异常处理,记录失败日志
  128. echo "错误信息: " . $e->getMessage() . "\n";
  129. echo "文件: " . $e->getFile() . "\n";
  130. echo "行号: " . $e->getLine() . "\n";
  131. // 删除当前任务
  132. $job->delete();
  133. }
  134. }
  135. }
  136. /**
  137. * 失败回调(可用于后续通知或重试机制)
  138. */
  139. public function failed($data)
  140. {
  141. echo "ImageJob failed: " . json_encode($data);
  142. }
  143. /**
  144. * 图生图主处理方法:根据产品图+参考图生成新图
  145. *
  146. * 业务分支:
  147. * - ProductImageGeneration:产品图创作(前端传 base64,先调模型成功后再落盘入库)
  148. * - ProductTemplateReplace:产品替换(读取已有产品图/模板图路径,生成效果图并返回前端展示)
  149. *
  150. * 入参说明($data):
  151. * - prompt: 提示词
  152. * - size: 尺寸(如 1:1、9:16、768x1024)
  153. * - status_val: 任务类型(图生图)
  154. * - status_type: 页面/流程类型(决定走哪个业务分支)
  155. * - product_img: 产品图(base64 或相对路径)
  156. * - template_img: 参考图(base64 或相对路径)
  157. * - model: 模型名称
  158. * - sys_id: 操作用户
  159. * - task_id: 可选,异步任务ID(用于写 Redis 状态)
  160. *
  161. * @param array $data 图生图任务参数
  162. * @return string|array 成功返回生成图相对路径;失败返回 ['code'=>1,'msg'=>错误信息]
  163. */
  164. public function get_img_to_img($data)
  165. {
  166. // 1) 基础参数解析
  167. $prompt = trim($data['prompt'] ?? '');
  168. $size = trim($data['size'] ?? '');
  169. $statusVal = trim($data['status_val'] ?? '');
  170. $statusType = trim($data['status_type'] ?? '');
  171. $productImg = trim($data['product_img'] ?? '');
  172. $templateImg = trim($data['template_img'] ?? '');
  173. $oldImg = trim($data['old_img'] ?? '');
  174. $refImg = trim($data['ref_img'] ?? '');
  175. $height = trim((string)($data['height'] ?? ''));
  176. $width = trim((string)($data['width'] ?? ''));
  177. $model = trim($data['model'] ?? '');
  178. $sysId = $data['sys_id'];
  179. $now = date('Y-m-d H:i:s');
  180. // texttoimage:前端传 old_img(base64/路径),size 为空时用 width x height
  181. if ($statusType === 'texttoimage') {
  182. if ($size === '' && $width !== '' && $height !== '') {
  183. $size = $width . 'x' . $height;
  184. }
  185. if ($size === '') {
  186. $size = '1:1';
  187. }
  188. }
  189. // 失败统一回写任务状态,避免多处重复代码
  190. $fail = function ($msg) use ($data) {
  191. if (!empty($data['task_id'])) {
  192. try {
  193. $redis = getTaskRedis();
  194. $redis->set("img_to_img_task:" . $data['task_id'], json_encode([
  195. 'status' => ImageService::TASK_STATUS_FAILED,
  196. 'msg' => $msg,
  197. 'error' => $msg,
  198. 'completed_at' => date('Y-m-d H:i:s')
  199. ], JSON_UNESCAPED_UNICODE), ['EX' => 300]);
  200. } catch (\Exception $e) {
  201. // Redis 不可用时不阻断主流程
  202. }
  203. }
  204. return ['code' => 1, 'msg' => $msg];
  205. };
  206. // 成功统一回写任务状态
  207. $complete = function ($imgPath) use ($data) {
  208. if (!empty($data['task_id'])) {
  209. try {
  210. $redis = getTaskRedis();
  211. $redis->set("img_to_img_task:" . $data['task_id'], json_encode([
  212. 'status' => ImageService::TASK_STATUS_COMPLETED,
  213. 'image' => $imgPath,
  214. 'image_url' => $imgPath,
  215. 'completed_at' => date('Y-m-d H:i:s')
  216. ], JSON_UNESCAPED_UNICODE), ['EX' => 300]);
  217. } catch (\Exception $e) {
  218. // Redis 不可用时不阻断主流程
  219. }
  220. }
  221. };
  222. // 2) 解析输入图片(按页面类型分支)
  223. $productBase64 = null;
  224. $productMime = 'image/png';
  225. $templateBase64 = null;
  226. $templateMime = 'image/png';
  227. $productExt = 'png';
  228. $templateExt = 'png';
  229. $sourceProductPath = '';
  230. $sourceTemplatePath = '';
  231. if ($statusType === 'ProductImageGeneration') {
  232. // 产品图创作:前端直接传 base64,先解析,等模型成功后再入库/落盘
  233. preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $productImg, $pm);
  234. if (empty($pm)) {
  235. return $fail('产品图未找到图片数据');
  236. }
  237. $productBase64 = preg_replace('/\s+/', '', $pm[2]);
  238. $productMime = ($pm[1] === 'jpg' ? 'image/jpeg' : 'image/' . $pm[1]);
  239. $productExt = $pm[1];
  240. preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $templateImg, $tm);
  241. if (empty($tm)) {
  242. return $fail('模板图未找到图片数据');
  243. }
  244. $templateBase64 = preg_replace('/\s+/', '', $tm[2]);
  245. $templateMime = ($tm[1] === 'jpg' ? 'image/jpeg' : 'image/' . $tm[1]);
  246. $templateExt = $tm[1];
  247. } elseif ($statusType === 'ProductTemplateReplace') {
  248. // 产品替换:优先转为 OSS 完整 URL 再读取,兼容库中存相对路径
  249. $productImgSource = Common::ossFullUrl((string)$productImg);
  250. $templateImgSource = Common::ossFullUrl((string)$templateImg);
  251. try {
  252. $productImgRaw = AIGatewayService::file_get_contents($productImgSource);
  253. $productBase64 = $productImgRaw['base64Data'];
  254. $productMime = $productImgRaw['mimeType'];
  255. $templateImgRaw = AIGatewayService::file_get_contents($templateImgSource);
  256. $templateBase64 = $templateImgRaw['base64Data'];
  257. $templateMime = $templateImgRaw['mimeType'];
  258. } catch (\Exception $e) {
  259. // 回传清晰错误,方便定位是产品图还是模板图读取失败
  260. return $fail('读取产品图/模板图失败: ' . $e->getMessage());
  261. }
  262. } elseif ($statusType === 'texttoimage') {
  263. // 前端传 old_img(原图 base64 或路径),ref_img(参考图,可选)
  264. $inputOldImg = $oldImg !== '' ? $oldImg : $productImg;
  265. $inputRefImg = $refImg !== '' ? $refImg : $templateImg;
  266. if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,(.+)$/is', $inputOldImg, $pm)) {
  267. $productBase64 = preg_replace('/\s+/', '', $pm[2]);
  268. $ext = strtolower($pm[1]);
  269. $productMime = ($ext === 'jpg' ? 'image/jpeg' : 'image/' . $ext);
  270. $productExt = ($ext === 'jpeg' ? 'jpg' : $ext);
  271. } elseif ($inputOldImg !== '') {
  272. $sourceProductPath = ltrim(str_replace('\\', '/', $inputOldImg), '/');
  273. try {
  274. $productImgSource = Common::ossFullUrl((string)$inputOldImg);
  275. $productImgRaw = AIGatewayService::file_get_contents($productImgSource);
  276. $productBase64 = $productImgRaw['base64Data'];
  277. $productMime = $productImgRaw['mimeType'];
  278. } catch (\Exception $e) {
  279. return $fail('读取原图失败: ' . $e->getMessage());
  280. }
  281. } else {
  282. return $fail('原图不能为空');
  283. }
  284. if ($inputRefImg !== '') {
  285. if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,(.+)$/is', $inputRefImg, $tm)) {
  286. $templateBase64 = preg_replace('/\s+/', '', $tm[2]);
  287. $ext = strtolower($tm[1]);
  288. $templateMime = ($ext === 'jpg' ? 'image/jpeg' : 'image/' . $ext);
  289. $templateExt = ($ext === 'jpeg' ? 'jpg' : $ext);
  290. } else {
  291. $sourceTemplatePath = ltrim(str_replace('\\', '/', $inputRefImg), '/');
  292. try {
  293. $templateImgSource = Common::ossFullUrl((string)$inputRefImg);
  294. $templateImgRaw = AIGatewayService::file_get_contents($templateImgSource);
  295. $templateBase64 = $templateImgRaw['base64Data'];
  296. $templateMime = $templateImgRaw['mimeType'];
  297. } catch (\Exception $e) {
  298. return $fail('读取参考图失败: ' . $e->getMessage());
  299. }
  300. }
  301. }
  302. } else {
  303. return $fail('当前页面未进行配置,请联系管理员开通权限');
  304. }
  305. // 3) 调模型生成图像
  306. if ($statusType === 'texttoimage') {
  307. $promptContent = $prompt !== '' ? $prompt : '请根据原图生成高质量图片';
  308. // 原图/参考图先落盘并上传 OSS(即使 AI 失败也能在 OSS 看到 texttoimage 目录)
  309. $rootPath = str_replace('\\', '/', ROOT_PATH);
  310. $dateSegment = date('Y-m-d');
  311. $saveDir = rtrim($rootPath, '/') . '/public/uploads/texttoimage/' . $dateSegment . '/';
  312. if (!is_dir($saveDir)) {
  313. mkdir($saveDir, 0755, true);
  314. }
  315. if ($sourceProductPath === '' && !empty($productBase64)) {
  316. $sourceFile = 'source-' . uniqid() . '.' . $productExt;
  317. $sourceImageData = base64_decode($productBase64);
  318. if ($sourceImageData === false || !file_put_contents($saveDir . $sourceFile, $sourceImageData)) {
  319. return $fail('原图保存失败');
  320. }
  321. $sourceProductPath = 'uploads/texttoimage/' . $dateSegment . '/' . $sourceFile;
  322. if (!Common::uploadLocalFileToOss((string)($saveDir . $sourceFile), (string)$sourceProductPath)) {
  323. echo 'OSS 上传原图失败: ' . $sourceProductPath . "\n";
  324. }
  325. }
  326. if ($sourceTemplatePath === '' && !empty($templateBase64)) {
  327. $refFile = 'ref-' . uniqid() . '.' . $templateExt;
  328. $refImageData = base64_decode($templateBase64);
  329. if ($refImageData !== false && file_put_contents($saveDir . $refFile, $refImageData)) {
  330. $sourceTemplatePath = 'uploads/texttoimage/' . $dateSegment . '/' . $refFile;
  331. if (!Common::uploadLocalFileToOss((string)($saveDir . $refFile), (string)$sourceTemplatePath)) {
  332. echo 'OSS 上传参考图失败: ' . $sourceTemplatePath . "\n";
  333. }
  334. }
  335. }
  336. } else {
  337. $defaultPrompt = '请完成产品模板替换:
  338. 1. 从产品图提取产品主体、品牌名称、核心文案;
  339. 2. 从模板图继承版式布局、文字排版、色彩风格、背景元素;
  340. 3. 将模板图中的产品和文字替换为产品图的内容;
  341. 4. 最终生成的图片与模板图视觉风格统一,仅替换产品和文字。';
  342. $promptContent = $prompt ? $prompt . "\n\n" . $defaultPrompt : $defaultPrompt;
  343. }
  344. $aiGateway = new AIGatewayService();
  345. // texttoimage:原图 + 提示词走图生图(gemini-3-pro-image-preview)
  346. $res = $aiGateway->buildRequestData(
  347. $statusVal,
  348. $model,
  349. $promptContent,
  350. $size,
  351. (string)$productBase64,
  352. $productMime,
  353. (string)$templateBase64,
  354. $templateMime
  355. );
  356. $generatedBase64 = $aiGateway->extractImageBase64FromResponse($res);
  357. if (!$generatedBase64) {
  358. $aiGateway->logImageResponseDebug($res, $data['task_id'] ?? '');
  359. return $fail($aiGateway->describeImageExtractFailure($res));
  360. }
  361. $generatedImageData = base64_decode($generatedBase64);
  362. if ($generatedImageData === false || strlen($generatedImageData) < 100) {
  363. return $fail('图片Base64解码失败');
  364. }
  365. // 4) 按业务分支落盘入库
  366. if ($statusType === 'ProductImageGeneration') {
  367. // 4.1 产品图创作:保存产品图/模板图/生成图 -> 插入 product_image_generate
  368. $rootPath = str_replace('\\', '/', ROOT_PATH);
  369. $saveDir = rtrim($rootPath, '/') . '/public/uploads/Product/' . date('Y-m-d') . '/';
  370. if (!is_dir($saveDir)) {
  371. mkdir($saveDir, 0755, true);
  372. }
  373. $productFile = 'product-' . uniqid() . '.' . $productExt;
  374. $productImageData = base64_decode($productBase64);
  375. if ($productImageData === false || !file_put_contents($saveDir . $productFile, $productImageData)) {
  376. return $fail('产品图保存失败');
  377. }
  378. $productDbPath = 'uploads/Product/' . date('Y-m-d') . '/' . $productFile;
  379. Common::uploadLocalFileToOss((string)($saveDir . $productFile), (string)$productDbPath);
  380. $templateFile = 'template-' . uniqid() . '.' . $templateExt;
  381. $templateImageData = base64_decode($templateBase64);
  382. if ($templateImageData === false || !file_put_contents($saveDir . $templateFile, $templateImageData)) {
  383. return $fail('模板图保存失败');
  384. }
  385. $templateDbPath = 'uploads/Product/' . date('Y-m-d') . '/' . $templateFile;
  386. Common::uploadLocalFileToOss((string)($saveDir . $templateFile), (string)$templateDbPath);
  387. $fileName = uniqid() . '.png';
  388. if (!file_put_contents($saveDir . $fileName, $generatedImageData)) {
  389. return $fail('生成图保存失败');
  390. }
  391. $generatedDbPath = 'uploads/Product/' . date('Y-m-d') . '/' . $fileName;
  392. Common::uploadLocalFileToOss((string)($saveDir . $fileName), (string)$generatedDbPath);
  393. Db::name('product_image_generate')->insert([
  394. 'prompt' => $prompt,
  395. 'model' => $model,
  396. 'product_img' => $productDbPath,
  397. 'reference_image' => $templateDbPath,
  398. 'generated_image' => $generatedDbPath,
  399. 'status_val' => $statusVal,
  400. 'size' => $size,
  401. 'sys_id' => $sysId,
  402. 'createTime' => $now,
  403. ]);
  404. $complete($generatedDbPath);
  405. return $generatedDbPath;
  406. } else if ($statusType === 'ProductTemplateReplace') {
  407. // 4.2 产品替换:生成图落盘到 merchant/newimg -> 回写 product + product_image
  408. $product = Db::name('product')->where('id', $data['id'])->find();
  409. if (empty($product)) {
  410. return $fail('产品不存在');
  411. }
  412. $productCode = $product['product_code'];
  413. $productPrefix = substr($productCode, 0, 9);
  414. $rootPath = str_replace('\\', '/', ROOT_PATH);
  415. $saveDir = rtrim($rootPath, '/') . '/public/uploads/merchant/' . $productPrefix . '/' . $productCode . '/newimg/';
  416. if (!is_dir($saveDir)) {
  417. mkdir($saveDir, 0755, true);
  418. }
  419. $fileName = 'img2img-' . date('YmdHis') . '-' . uniqid() . '.png';
  420. $fullPath = $saveDir . $fileName;
  421. if (!file_put_contents($fullPath, $generatedImageData)) {
  422. return $fail('图片保存失败');
  423. }
  424. $dbImgPath = 'uploads/merchant/' . $productPrefix . '/' . $productCode . '/newimg/' . $fileName;
  425. Common::uploadLocalFileToOss((string)$fullPath, (string)$dbImgPath);
  426. Db::name('product')->where('id', $data['id'])->update([
  427. 'createTime' => date('Y-m-d H:i:s'),
  428. 'content' => $data['prompt'],
  429. 'product_new_img' => $dbImgPath
  430. ]);
  431. Db::name('product_image')->insert([
  432. 'product_id' => $data['id'],
  433. 'product_new_img' => $dbImgPath,
  434. 'product_content' => $data['prompt'],
  435. 'template_id' => $data['template_id'],
  436. 'createTime' => date('Y-m-d H:i:s'),
  437. ]);
  438. $complete($dbImgPath);
  439. return $dbImgPath;
  440. } elseif ($statusType === 'texttoimage') {
  441. $rootPath = str_replace('\\', '/', ROOT_PATH);
  442. $dateSegment = date('Y-m-d');
  443. $saveDir = rtrim($rootPath, '/') . '/public/uploads/texttoimage/' . $dateSegment . '/';
  444. if (!is_dir($saveDir)) {
  445. mkdir($saveDir, 0755, true);
  446. }
  447. $fileName = 'text2img-' . date('YmdHis') . '-' . uniqid() . '.png';
  448. $fullPath = $saveDir . $fileName;
  449. if (!file_put_contents($fullPath, $generatedImageData)) {
  450. return $fail('图片保存失败');
  451. }
  452. $dbImgPath = 'uploads/texttoimage/' . $dateSegment . '/' . $fileName;
  453. if (!Common::uploadLocalFileToOss((string)$fullPath, (string)$dbImgPath)) {
  454. echo 'OSS 上传生成图失败: ' . $dbImgPath . "\n";
  455. }
  456. // size 字段为 double:优先存 width,其次 size 数值
  457. $sizeForDb = 0;
  458. if ($width !== '' && is_numeric($width)) {
  459. $sizeForDb = (float)$width;
  460. } elseif ($size !== '' && is_numeric($size)) {
  461. $sizeForDb = (float)$size;
  462. }
  463. Db::name('ai_text_image')->insert([
  464. 'old_img' => $sourceProductPath,
  465. 'ref_img' => $sourceTemplatePath,
  466. 'new_img' => $dbImgPath,
  467. 'prompt' => $prompt,
  468. 'size' => $sizeForDb,
  469. 'sys_id' => $sysId,
  470. 'sys_rq' => $now,
  471. ]);
  472. $complete($dbImgPath);
  473. return $dbImgPath;
  474. } else {
  475. return $fail('当前页面未进行配置,请联系管理员开通权限');
  476. }
  477. }
  478. public function ImageToImage($fileName, $outputDirRaw, $new_image_url, $width, $height)
  479. {
  480. $rootPath = str_replace('\\', '/', ROOT_PATH);
  481. $outputDir = rtrim($rootPath . 'public/' . ltrim($outputDirRaw, '/'), '/') . '/';
  482. $dateDir = date('Y-m-d') . '/';
  483. $fullBaseDir = $outputDir . $dateDir;
  484. // 创建主目录和 imgtoimg 子目录
  485. if (!is_dir($fullBaseDir)) {
  486. mkdir($fullBaseDir, 0755, true);
  487. }
  488. $imgtoimgDir = $fullBaseDir . '1024x1303/';
  489. if (!is_dir($imgtoimgDir)) {
  490. mkdir($imgtoimgDir, 0755, true);
  491. }
  492. // 查询数据库原图记录
  493. $record = Db::name('text_to_image')
  494. ->where('old_image_url', 'like', "%{$fileName}")
  495. ->order('id desc')
  496. ->find();
  497. if (!$record) {
  498. return json(['code' => 1, 'msg' => '没有找到匹配的图像记录']);
  499. }
  500. // 调用 AI 图生图 API
  501. $ai = new AIGatewayService();
  502. $res = $ai->txt2imgWithControlNet('', $new_image_url);
  503. if (!isset($res['code']) || $res['code'] !== 0) {
  504. return json(['code' => 1, 'msg' => $res['msg'] ?? '图像生成失败']);
  505. }
  506. // 生成保存文件路径
  507. $originalBaseName = pathinfo($new_image_url, PATHINFO_FILENAME);
  508. $finalFileName = $originalBaseName . '.png';
  509. $savePath = $imgtoimgDir . $finalFileName;
  510. // 写入图像文件
  511. if (!file_put_contents($savePath, base64_decode($res['data']['base64']))) {
  512. return json(['code' => 1, 'msg' => '图像保存失败,请检查目录权限']);
  513. }
  514. // 图生图结果同步 OSS(失败不阻断)
  515. $relativeImgPath = rtrim($outputDirRaw, '/') . '/' . $dateDir . '1024x1303/' . $finalFileName;
  516. Common::uploadLocalFileToOss((string)$savePath, (string)$relativeImgPath);
  517. // 构造相对路径用于数据库
  518. // 更新数据库记录
  519. Db::name('text_to_image')->where('id', $record['id'])->update([
  520. 'imgtoimg_url' => $relativeImgPath,
  521. 'status_name' => '图生图',
  522. 'error_msg' => '',
  523. 'update_time' => date('Y-m-d H:i:s')
  524. ]);
  525. // 返回成功响应
  526. return "成功";
  527. }
  528. }