ImageToImageJob.php 20 KB

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