WorkOrder.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. <?php
  2. namespace app\api\controller;
  3. use app\common\controller\Api;
  4. use app\job\ImageJob;
  5. use app\service\ImageService;
  6. use think\App;
  7. use think\Db;
  8. use think\Log;
  9. use think\Queue;
  10. use think\queue\job\Redis;
  11. class WorkOrder extends Api
  12. {
  13. protected $noNeedLogin = ['*'];
  14. protected $noNeedRight = ['*'];
  15. /**
  16. * 出图接口
  17. */
  18. public function imageToText()
  19. {
  20. $params = $this->request->param();
  21. $service = new ImageService();
  22. $service->handleImage($params);
  23. $this->success('成功存入队列中');
  24. }
  25. /**
  26. * 开启队列任务
  27. */
  28. public function kaiStats()
  29. {
  30. // 判断是否已有监听进程在运行
  31. $check = shell_exec("ps aux | grep 'queue:listen' | grep -v grep");
  32. if ($check) {
  33. return json([
  34. 'code' => 1,
  35. 'msg' => '监听进程已存在,请勿重复启动'
  36. ]);
  37. }
  38. // 启动监听
  39. // $command = 'php think queue:listen --queue --timeout=300 --sleep=3 --memory=256 > /dev/null 2>&1 &';
  40. $command = 'nohup php think queue:listen --queue --timeout=300 --sleep=3 --memory=256 > /var/log/job_queue.log 2>&1 &';
  41. exec($command, $output, $status);
  42. if ($status === 0) {
  43. return json([
  44. 'code' => 0,
  45. 'msg' => '队列监听已启动'
  46. ]);
  47. } else {
  48. return json([
  49. 'code' => 1,
  50. 'msg' => '队列启动失败',
  51. 'output' => $output
  52. ]);
  53. }
  54. }
  55. /**
  56. * 查看队列任务
  57. */
  58. public function queueStats()
  59. {
  60. $statusCounts = Db::name('queue_log')
  61. ->field('status, COUNT(*) as total')
  62. ->whereTime('created_at', 'today')
  63. ->where('status','<>', 4)
  64. ->group('status')
  65. ->select();
  66. $result = [
  67. '待处理' => 0,
  68. '处理中' => 0,
  69. '成功' => 0,
  70. '失败' => 0
  71. ];
  72. $total = 0;
  73. foreach ($statusCounts as $row) {
  74. $count = $row['total'];
  75. $total += $count;
  76. switch ($row['status']) {
  77. case 0:
  78. $result['待处理'] = $count;
  79. break;
  80. case 1:
  81. $result['处理中'] = $count;
  82. break;
  83. case 2:
  84. $result['成功'] = $count;
  85. break;
  86. case 3:
  87. $result['失败'] = $count;
  88. break;
  89. }
  90. }
  91. return json([
  92. 'code' => 0,
  93. 'msg' => '获取成功',
  94. 'data' => ['总任务数' => $total] + $result
  95. ]);
  96. }
  97. /**
  98. * 清空停止队列(同时删除近30分钟的队列日志,并停止 systemd 队列服务)
  99. */
  100. public function stopQueueProcesses()
  101. {
  102. try {
  103. // 连接 Redis
  104. $redis = new \Redis();
  105. $redis->connect('127.0.0.1', 6379);
  106. $redis->select(15);
  107. $key = 'queues:default';
  108. $count = $redis->lLen($key);
  109. // 计算时间:当前时间前 30 分钟
  110. $time = date('Y-m-d H:i:s', strtotime('-30 minutes'));
  111. // 删除数据库中状态为0且在近30分钟的数据
  112. $deleteCount = Db::name('queue_log')
  113. ->where('status', 0)
  114. ->where('created_at', '>=', $time)
  115. ->delete();
  116. // 如果 Redis 队列不为空,则清空
  117. if ($count > 0) {
  118. $redis->del($key);
  119. }
  120. // 尝试停止 systemd 队列服务
  121. exec('sudo /bin/systemctl stop think-queue.service', $output, $status);
  122. if ($status === 0) {
  123. return json([
  124. 'code' => 0,
  125. 'msg' => "队列服务已成功停止"
  126. ]);
  127. } else {
  128. return json([
  129. 'code' => 2,
  130. 'msg' => "但服务停止失败,请检查权限"
  131. ]);
  132. }
  133. } catch (\Exception $e) {
  134. return json([
  135. 'code' => 500,
  136. 'msg' => '处理异常:' . $e->getMessage()
  137. ]);
  138. }
  139. }
  140. // public function stopQueueProcesses()
  141. // {
  142. // $redis = new \Redis();
  143. // $redis->connect('127.0.0.1', 6379);
  144. // $redis->select(15);
  145. //
  146. // $key = 'queues:default';
  147. // $count = $redis->lLen($key);
  148. // // 计算时间:当前时间前30分钟
  149. // $time = date('Y-m-d H:i:s', strtotime('-30 minutes'));
  150. //
  151. // if ($count === 0) {
  152. // // 删除数据库中状态为0且 created_at 在最近30分钟的数据
  153. // $deleteCount = Db::name('queue_log')
  154. // ->where('created_at', '>=', $time)
  155. // ->delete();
  156. // return json([
  157. // 'code' => 1,
  158. // 'msg' => '暂无队列需要停止'
  159. // ]);
  160. // }
  161. //
  162. // // 清空 Redis 队列
  163. // $redis->del($key);
  164. //
  165. //
  166. //
  167. // // 删除数据库中状态为0且 created_at 在最近30分钟的数据
  168. // $deleteCount = Db::name('queue_log')
  169. // ->where('created_at', '>=', $time)
  170. // ->delete();
  171. //
  172. // return json([
  173. // 'code' => 0,
  174. // 'msg' => '已成功停止队列任务'
  175. // ]);
  176. // }
  177. /**
  178. * 显示当前运行中的队列监听进程
  179. */
  180. public function viewQueueStatus()
  181. {
  182. $redis = new \Redis();
  183. $redis->connect('127.0.0.1', 6379);
  184. $redis->select(15);
  185. $key = 'queues:default';
  186. $count = $redis->lLen($key);
  187. $list = $redis->lRange($key, 0, 9);
  188. $parsed = array_map(function ($item) {
  189. return json_decode($item, true);
  190. }, $list);
  191. return json([
  192. 'code' => 0,
  193. 'msg' => '查询成功',
  194. 'count' => $count,
  195. 'tasks_preview' => $parsed
  196. ]);
  197. }
  198. // public function imageToTexts()
  199. // {
  200. // $params = $this->request->param();
  201. //
  202. // // 统一路径格式
  203. // $sourceDirRaw = str_replace('\\', '/', trim($params['sourceDir'] ?? '', '/'));
  204. // $fileName = trim($params['file_name'] ?? '');
  205. //
  206. // // 自动拆分文件名
  207. // if (!$fileName && preg_match('/([^\/]+\.(jpg|jpeg|png))$/i', $sourceDirRaw, $matches)) {
  208. // $fileName = $matches[1];
  209. // $sourceDirRaw = preg_replace('/\/' . preg_quote($fileName, '/') . '$/', '', $sourceDirRaw);
  210. // }
  211. //
  212. // // 参数校验
  213. // if ($sourceDirRaw === '' || $fileName === '') {
  214. // return $this->error('参数错误:sourceDir 或 file_name 不能为空');
  215. // }
  216. //
  217. // // 构建路径
  218. // $rootPath = str_replace('\\', '/', ROOT_PATH);
  219. // $sourceDir = rtrim($rootPath . 'public/' . $sourceDirRaw, '/') . '/';
  220. // $filePath = $sourceDir . $fileName;
  221. // $relativePath = $sourceDirRaw . '/' . $fileName;
  222. //
  223. // // 文件检查
  224. // if (!is_dir($sourceDir)) {
  225. // return $this->error('源目录不存在:' . $sourceDir);
  226. // }
  227. // if (!is_file($filePath)) {
  228. // return $this->error('文件不存在:' . $filePath);
  229. // }
  230. //
  231. // // 避免重复处理
  232. //// $exists = Db::name('text_to_image')
  233. //// ->where('old_image_url', $relativePath)
  234. //// ->where('chinese_description', '<>', '')
  235. //// ->find();
  236. //// if ($exists) {
  237. //// return $this->success('该图片已生成描述,无需重复处理');
  238. //// }
  239. //
  240. //// try {
  241. // // 获取图片信息
  242. // $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
  243. // $mime = ($ext === 'jpg' || $ext === 'jpeg') ? 'jpeg' : $ext;
  244. //
  245. // list($width, $height) = getimagesize($filePath);
  246. // $imageData = base64_encode(file_get_contents($filePath));
  247. // if (!$imageData || strlen($imageData) < 1000) {
  248. // throw new \Exception('图片内容读取失败');
  249. // }
  250. // $imageUrl = "data:image/{$mime};base64,{$imageData}";
  251. //
  252. // // 构建严格格式的提示词
  253. // $userPrompt = preg_replace('/\s+/u', '', $params['prompt']); // 移除所有空白字符
  254. // $strictPrompt = "严格遵守以下规则:
  255. //1. 只返回三段内容:
  256. // 第一段:纯中文图案描述
  257. // 第二段:---json json---
  258. // 第三段:纯英文图案描述
  259. //2. 描述中必须体现图案的类型、颜色、风格等关键信息
  260. //3. 不允许添加任何解释、引导、说明、示例等文字,必须只包含图案描述内容本身
  261. //3. 示例:
  262. //这张图中的图案是代表达拉斯足球队的标志,包括一个头盔图形和围绕它的文字。头盔以灰色和白色为主,有蓝色和黑色的细节。
  263. //---json json---
  264. //The pattern in this picture is the logo representing the Dallas football team, including a helmet figure and the text around it. The helmet is mainly gray and white, with blue and black details.
  265. //请直接描述这个图案:
  266. //" . $userPrompt;
  267. //
  268. // // 调用 GPT 接口
  269. // $gptRes = $this->callGptApi($imageUrl, $strictPrompt);
  270. // $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
  271. //
  272. // // 验证 GPT 返回格式
  273. // if (strpos($gptText, '---json json---') === false) {
  274. // throw new \Exception('GPT 返回格式不正确,缺少分隔符');
  275. // }
  276. //
  277. // list($chineseDesc, $englishDesc) = array_map('trim', explode('---json json---', $gptText));
  278. //
  279. // if ($chineseDesc === '' || $englishDesc === '') {
  280. // throw new \Exception('描述内容为空,请检查 GPT 返回');
  281. // }
  282. //
  283. // // 插入数据库(成功时才插入)
  284. // $this->logToDatabase([
  285. // 'old_image_url' => $relativePath,
  286. // 'chinese_description' => $chineseDesc,
  287. // 'english_description' => $englishDesc,
  288. // 'size' => "",
  289. // 'status' => 1
  290. // ]);
  291. //
  292. // return $this->success('图生文成功', [
  293. // 'chinese' => $chineseDesc,
  294. // 'english' => $englishDesc
  295. // ]);
  296. //
  297. //// } catch (\Exception $e) {
  298. //// // 只写日志,不重复插入数据库
  299. ////// Log::error('图生文失败 [' . $relativePath . ']:' . $e->getMessage());
  300. //// Db::name('text_to_image')->insert([
  301. //// 'old_image_url' => $relativePath,
  302. //// 'error_msg' => $e->getMessage(),
  303. //// 'status' => 0,
  304. //// 'create_time' => date('Y-m-d H:i:s') // 可选:记录时间戳
  305. //// ]);
  306. //// return json([
  307. //// 'code' => 1,
  308. //// 'msg' => '图生文失败:' . $e->getMessage()
  309. //// ]);
  310. //// }
  311. // }
  312. //
  313. // /**
  314. // * 调用GPT API生成文字描述(改进版)
  315. // */
  316. // public function callGptApi($imageUrl, $prompt)
  317. // {
  318. // $data = [
  319. // "model" => "gpt-4-vision-preview",
  320. // "messages" => [[
  321. // "role" => "user",
  322. // "content" => [
  323. // ["type" => "text", "text" => $prompt],
  324. // ["type" => "image_url", "image_url" => [
  325. // "url" => $imageUrl,
  326. // "detail" => "auto" // ✅ 显式添加 detail 字段,兼容 vision API
  327. // ]]
  328. // ]
  329. // ]],
  330. // "max_tokens" => 1000
  331. // ];
  332. //
  333. // return $this->callApi($this->config['gpt']['api_url'], $this->config['gpt']['api_key'], $data);
  334. // }
  335. //
  336. //
  337. // /**
  338. // * 文字生成图片(第二步) - 等比缩放生成指定尺寸图
  339. // */
  340. // public function textToImage()
  341. // {
  342. // $params = $this->request->param();
  343. // $fileName = trim($params['file_name'] ?? '');
  344. // $outputDirRaw = trim($params['outputDir'] ?? '', '/');
  345. // $width = intval($params['width'] ?? 512);
  346. // $height = intval($params['height'] ?? 512);
  347. // $prompt = trim($params['prompt'] ?? '');
  348. //
  349. // // 统一路径格式
  350. // $rootPath = str_replace('\\', '/', ROOT_PATH);
  351. // $outputDir = rtrim($rootPath . 'public/' . $outputDirRaw, '/') . '/';
  352. // $dateDir = date('Y-m-d') . '/';
  353. // $fullBaseDir = $outputDir . $dateDir;
  354. //
  355. // foreach ([$fullBaseDir, $fullBaseDir . '1024x1024/', $fullBaseDir . "{$width}x{$height}/"] as $dir) {
  356. // if (!is_dir($dir)) {
  357. // mkdir($dir, 0755, true);
  358. // }
  359. // }
  360. //
  361. // // 提示词合法性校验
  362. // $prompt = preg_replace('/[\r\n\t]+/', ' ', $prompt);
  363. //// if (empty($prompt) || mb_strlen($prompt) < 10) {
  364. //// return json(['code' => 1, 'msg' => '提示词过短或为空,请填写有效的英文图像描述']);
  365. //// }
  366. //// if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $prompt)) {
  367. //// return json(['code' => 1, 'msg' => '请使用英文提示词,当前提示内容包含中文']);
  368. //// }
  369. //
  370. // // 查询图像记录
  371. // $record = Db::name('text_to_image')
  372. // ->where('old_image_url', 'like', "%{$fileName}")
  373. // ->order('id desc')
  374. // ->find();
  375. //// echo "<pre>";
  376. //// print_r($record);
  377. //// echo "<pre>";
  378. //
  379. // if (!$record) {
  380. // return json(['code' => 1, 'msg' => '没有找到匹配的图像记录']);
  381. // }
  382. //
  383. //// try {
  384. // // 日志记录
  385. // $logDir = ROOT_PATH . 'runtime/logs/';
  386. // if (!is_dir($logDir)) mkdir($logDir, 0755, true);
  387. // file_put_contents($logDir . 'prompt_log.txt', date('Y-m-d H:i:s') . " prompt: {$prompt}\n", FILE_APPEND);
  388. //
  389. // // 调用 DALL·E API
  390. // $dalle1024 = $this->callDalleApi($prompt);
  391. //
  392. // file_put_contents($logDir . 'dalle_response.log', date('Y-m-d H:i:s') . "\n" . print_r($dalle1024, true) . "\n", FILE_APPEND);
  393. //
  394. // if (!isset($dalle1024['data'][0]['url']) || empty($dalle1024['data'][0]['url'])) {
  395. // $errorText = $dalle1024['error']['message'] ?? '未知错误';
  396. // throw new \Exception('DALL·E 生成失败:' . $errorText);
  397. // }
  398. //
  399. // $imgUrl1024 = $dalle1024['data'][0]['url'];
  400. // $imgData1024 = @file_get_contents($imgUrl1024);
  401. // if (!$imgData1024 || strlen($imgData1024) < 1000) {
  402. // throw new \Exception("下载图像失败或内容异常");
  403. // }
  404. //
  405. // // 保存原图
  406. // $filename1024 = 'dalle_' . md5($record['old_image_url'] . microtime()) . '_1024.png';
  407. // $savePath1024 = $fullBaseDir . '1024x1024/' . $filename1024;
  408. // file_put_contents($savePath1024, $imgData1024);
  409. //
  410. // // 创建图像资源
  411. // $im = @imagecreatefromstring($imgData1024);
  412. // if (!$im) {
  413. // throw new \Exception("图像格式不受支持或已损坏");
  414. // }
  415. //
  416. //// 获取原图尺寸
  417. // $srcWidth = imagesx($im);
  418. // $srcHeight = imagesy($im);
  419. //
  420. //// 目标尺寸
  421. // $targetWidth = $width;
  422. // $targetHeight = $height;
  423. //
  424. //// 计算缩放比例(以覆盖为目标,可能会超出一边)
  425. // $ratio = max($targetWidth / $srcWidth, $targetHeight / $srcHeight);
  426. // $newWidth = intval($srcWidth * $ratio);
  427. // $newHeight = intval($srcHeight * $ratio);
  428. //
  429. //// 创建目标图像(目标尺寸)
  430. // $dstImg = imagecreatetruecolor($targetWidth, $targetHeight);
  431. //
  432. //// 缩放后居中裁剪偏移
  433. // $offsetX = intval(($newWidth - $targetWidth) / 2);
  434. // $offsetY = intval(($newHeight - $targetHeight) / 2);
  435. //
  436. //// 临时缩放图像
  437. // $tempImg = imagecreatetruecolor($newWidth, $newHeight);
  438. // imagecopyresampled($tempImg, $im, 0, 0, 0, 0, $newWidth, $newHeight, $srcWidth, $srcHeight);
  439. //
  440. //// 裁剪中间部分到最终尺寸
  441. // imagecopy($dstImg, $tempImg, 0, 0, $offsetX, $offsetY, $targetWidth, $targetHeight);
  442. //
  443. //// 保存结果
  444. // $filenameCustom = 'dalle_' . md5($record['old_image_url'] . microtime()) . "_custom.png";
  445. // $savePathCustom = $fullBaseDir . "{$width}x{$height}/" . $filenameCustom;
  446. // imagepng($dstImg, $savePathCustom);
  447. //
  448. //// 释放内存
  449. // imagedestroy($im);
  450. // imagedestroy($tempImg);
  451. // imagedestroy($dstImg);
  452. //
  453. // // 更新数据库
  454. // Db::name('text_to_image')->where('id', $record['id'])->update([
  455. // 'new_image_url' => str_replace($rootPath . 'public/', '', $savePath1024),
  456. // 'custom_image_url' => str_replace($rootPath . 'public/', '', $savePathCustom),
  457. // 'error_msg' => '',
  458. // 'size' => "{$width}x{$height}",
  459. // 'updated_time' => date('Y-m-d H:i:s')
  460. // ]);
  461. //
  462. // return json([
  463. // 'code' => 0,
  464. // 'msg' => '文生图生成完成',
  465. // 'data' => [
  466. // 'new_image_url' => str_replace($rootPath . 'public/', '', $savePath1024)?? '',
  467. // 'custom_image_url' => str_replace($rootPath . 'public/', '', $savePathCustom)?? '',
  468. // 'size' => "{$width}x{$height}"
  469. // ]
  470. // ]);
  471. //
  472. //// } catch (\Exception $e) {
  473. //// Db::name('text_to_image')->where('id', $record['id'])->update([
  474. //// 'status' => 0,
  475. //// 'error_msg' => $e->getMessage()
  476. //// ]);
  477. ////
  478. //// return json(['code' => 1, 'msg' => '文生图失败:' . $e->getMessage()]);
  479. //// }
  480. // }
  481. protected $config = [
  482. 'gpt' => [
  483. 'api_key' => 'sk-Bhos1lXTRpZiAAmN06624a219a874eCd91Dc068b902a3e73',
  484. 'api_url' => 'https://one.opengptgod.com/v1/chat/completions'
  485. ],
  486. 'dalle' => [
  487. 'api_key' => 'sk-e0JuPjMntkbgi1BoMjrqyyzMKzAxILkQzyGMSy3xiMupuoWY',
  488. 'api_url' => 'https://niubi.zeabur.app/v1/images/generations'
  489. ]
  490. ];
  491. public function callDallesssss()
  492. {
  493. // 确保目录存在
  494. $rootPath = str_replace('\\', '/', ROOT_PATH);
  495. $imgDir = rtrim($rootPath . 'public/' . '/uploads/img/', '/') . '/';
  496. if (!is_dir($imgDir)) mkdir($imgDir, 0755, true);
  497. $filename = 'dalle_' . date('Ymd_His') . '_' . rand(1000, 9999) . '.png';
  498. $savePath = $imgDir . $filename;
  499. $prompt = "这幅图案呈现了一棵精致的树木,树干与枝条采用金色设计,树上盛开着纯白色的花朵。花瓣层层叠加,呈现出优雅的立体感,花心部分呈现细腻的黄色。树叶也以金色为主,整体色调偏向温暖的金白色,背景简洁纯净,整体给人一种高雅且现代的艺术感";
  500. $response = $this->callDalleApi($prompt);
  501. if (!isset($response['data'][0]['url'])) {
  502. throw new \Exception("图像生成失败,未返回图片链接。返回内容:" . json_encode($response));
  503. }
  504. $imageUrl = $response['data'][0]['url'];
  505. // 下载图片到本地目录
  506. $imgContent = file_get_contents($imageUrl);
  507. if ($imgContent === false) {
  508. throw new \Exception("无法下载生成的图像:{$imageUrl}");
  509. }
  510. file_put_contents($savePath, $imgContent);
  511. // 日志记录
  512. $logDir = $rootPath . 'runtime/logs/';
  513. if (!is_dir($logDir)) mkdir($logDir, 0755, true);
  514. file_put_contents($logDir . 'prompt_log.txt', date('Y-m-d H:i:s') . " prompt: {$prompt}\nURL: {$imageUrl}\n", FILE_APPEND);
  515. echo "图像生成成功:public/uploads/img/{$filename}\n";
  516. }
  517. //
  518. // /**
  519. // * 调用 DALL·E 接口
  520. // * 文生图
  521. // */
  522. public function callDalleApi($prompt)
  523. {
  524. // $data = [
  525. // 'prompt' => $prompt,
  526. // 'model' => 'dall-e-2',
  527. // 'n' => 1,
  528. // 'size' => '1024x1024'
  529. // ];
  530. // return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
  531. $data = [
  532. 'prompt' => $prompt,
  533. 'model' => 'dall-e-2',
  534. // 'model' => 'gpt-image-1',
  535. 'n' => 1,
  536. 'size' => '1024x1024',
  537. 'quality' => 'standard',
  538. 'style' => 'vivid',
  539. 'response_format' => 'url'
  540. ];
  541. return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
  542. }
  543. /**
  544. * 通用API调用方法
  545. */
  546. public function callApi($url, $apiKey, $data)
  547. {
  548. $maxRetries = 2;
  549. $attempt = 0;
  550. $lastError = '';
  551. while ($attempt <= $maxRetries) {
  552. $ch = curl_init();
  553. curl_setopt_array($ch, [
  554. CURLOPT_URL => $url,
  555. CURLOPT_RETURNTRANSFER => true,
  556. CURLOPT_POST => true,
  557. CURLOPT_POSTFIELDS => json_encode($data),
  558. CURLOPT_HTTPHEADER => [
  559. 'Content-Type: application/json',
  560. 'Authorization: Bearer ' . $apiKey
  561. ],
  562. CURLOPT_TIMEOUT => 120,
  563. CURLOPT_SSL_VERIFYPEER => false,
  564. CURLOPT_SSL_VERIFYHOST => 0,
  565. CURLOPT_TCP_KEEPALIVE => 1,
  566. CURLOPT_FORBID_REUSE => false
  567. ]);
  568. $response = curl_exec($ch);
  569. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  570. $curlError = curl_error($ch);
  571. curl_close($ch);
  572. if ($response !== false && $httpCode === 200) {
  573. $result = json_decode($response, true);
  574. return $result;
  575. }
  576. $lastError = $curlError ?: "HTTP错误:{$httpCode}";
  577. $attempt++;
  578. sleep(1);
  579. }
  580. throw new \Exception("请求失败(重试{$maxRetries}次):{$lastError}");
  581. }
  582. //
  583. // /**
  584. // * 记录到数据库
  585. // */
  586. // public function logToDatabase($data)
  587. // {
  588. // $record = [
  589. // 'old_image_url' => $data['old_image_url'] ?? '',
  590. // 'new_image_url' => $data['new_image_url'] ?? '',
  591. // 'custom_image_url' => $data['custom_image_url'] ?? '',
  592. // 'size' => isset($data['image_width'], $data['image_height']) ?
  593. // $data['image_width'] . 'x' . $data['image_height'] : '',
  594. // 'chinese_description' => $data['chinese_description'] ?? '',
  595. // 'english_description' => $data['english_description'] ?? '',
  596. // 'model' => 'dall-e-2',
  597. // 'quality' => 'standard',
  598. // 'style' => 'vivid',
  599. // 'status' => $data['status'] ?? 0,
  600. // 'error_msg' => $data['error_msg'] ?? '',
  601. // 'created_time' => date('Y-m-d H:i:s'),
  602. // 'updated_time' => date('Y-m-d H:i:s')
  603. // ];
  604. //
  605. // if (isset($data['id'])) {
  606. // Db::name('text_to_image')->where('id', $data['id'])->update($record);
  607. // } else {
  608. // Db::name('text_to_image')->insert($record);
  609. // }
  610. // }
  611. //
  612. // /**
  613. // * 获取待处理的源图片列表
  614. // */
  615. // public function getSourceImages()
  616. // {
  617. // $params = $this->request->param();
  618. // $sourceDir = rtrim(ROOT_PATH . 'public/' . $params['sourceDir'], '/') . '/';
  619. //
  620. // if (!is_dir($sourceDir)) return $this->error('源目录不存在');
  621. //
  622. // $files = glob($sourceDir . '*.{jpg,jpeg,png}', GLOB_BRACE);
  623. // $result = [];
  624. //
  625. // foreach ($files as $file) {
  626. // $relativePath = trim($params['sourceDir'], '/') . '/' . basename($file);
  627. //
  628. // $exists = Db::name('text_to_image')
  629. // ->where('old_image_url', $relativePath)
  630. // ->where('status', 1)
  631. // ->find();
  632. //
  633. // if (!$exists) {
  634. // $result[] = basename($file);
  635. // }
  636. // }
  637. //
  638. // return $this->success('获取成功', $result);
  639. // }
  640. //
  641. // /**
  642. // * 获取处理成功的列表
  643. // */
  644. // public function getlist()
  645. // {
  646. // $today = date('Y-m-d');
  647. // $tomorrow = date('Y-m-d', strtotime('+1 day'));
  648. //
  649. // $res = Db::name('text_to_image')
  650. // ->where('status', 1)
  651. // ->where('created_time', '>=', $today . ' 00:00:00')
  652. // ->where('created_time', '<', $tomorrow . ' 00:00:00')
  653. // ->order('id desc')
  654. // ->select();
  655. //
  656. // foreach ($res as &$item) {
  657. // $item['status_text'] = '成功';
  658. // }
  659. //
  660. // return json(['code' => 0, 'msg' => '获取成功', 'data' => $res]);
  661. // }
  662. //
  663. // /**
  664. // * 获取错误日志
  665. // */
  666. // public function getErrorLogs()
  667. // {
  668. // $res = Db::name('text_to_image')
  669. // ->where('status', 0)
  670. // ->order('id desc')
  671. // ->limit(20)
  672. // ->select();
  673. //
  674. // return json(['code' => 0, 'msg' => '获取失败记录成功', 'data' => $res]);
  675. // }
  676. }