liuhairui 6 сар өмнө
parent
commit
d7fc17b254

+ 7 - 0
application/api/controller/Facility.php

@@ -80,6 +80,7 @@ class Facility extends Api
         $res = Db::name('text_to_image')->alias('b')
             ->field('b.chinese_description,b.english_description,b.new_image_url,b.custom_image_url,b.size,b.old_image_url,b.img_name')
             ->where('old_image_url', $params)
+            ->where('img_name', '<>', '')
             ->where('custom_image_url', '<>', '')
             ->where('status', 1)
             ->order('b.id desc')
@@ -120,7 +121,9 @@ class Facility extends Api
         // 2. 提前构建是否已出图map
         $dbRecords = Db::name('text_to_image')
             ->whereIn('old_image_url', $relativeImages)
+            ->where('img_name', '<>', '')
             ->where('custom_image_url', '<>', '')
+            ->where('status',1)
             ->field('old_image_url,new_image_url,custom_image_url,chinese_description,english_description,img_name')
             ->select();
 
@@ -132,8 +135,10 @@ class Facility extends Api
         // 3. 提前获取 same_count 的统计
         $sameCountMap = Db::name('text_to_image')
             ->whereIn('old_image_url', $relativeImages)
+            ->where('img_name', '<>', '')
             ->where('custom_image_url', '<>', '')
             ->group('old_image_url')
+            ->where('status',1)
             ->column('count(*) as cnt', 'old_image_url');
 
         // 4. 构造最终筛选数据(分页前进行状态筛选)
@@ -225,7 +230,9 @@ class Facility extends Api
                             // 数据库统计:已处理图片数量
                             $hasData = Db::name('text_to_image')
                                 ->where('custom_image_url', '<>', '')
+                                ->where('img_name', '<>', '')
                                 ->whereLike('old_image_url', $relativeDir . '/%')
+                                ->where('status',1)
                                 ->whereNotNull('custom_image_url')
                                 ->count();
 

+ 121 - 184
application/api/controller/WorkOrder.php

@@ -19,7 +19,7 @@ class WorkOrder extends Api
 
     /**
      * 出图接口
-    */
+     */
     public function imageToText()
     {
         $params = $this->request->param();
@@ -28,6 +28,7 @@ class WorkOrder extends Api
         $this->success('成功存入队列中');
     }
 
+
     /**
      * 开启队列任务
      */
@@ -107,93 +108,48 @@ class WorkOrder extends Api
     }
 
     /**
-     * 清空停止队列(同时删除近30分钟的队列日志,并停止 systemd 队列服务
+     * 清空队列(同时删除近30分钟的队列日志)
      */
     public function stopQueueProcesses()
     {
-        try {
-            // 连接 Redis
-            $redis = new \Redis();
-            $redis->connect('127.0.0.1', 6379);
-            $redis->select(15);
-
-            $key = 'queues:default';
-            $count = $redis->lLen($key);
+        $redis = new \Redis();
+        $redis->connect('127.0.0.1', 6379);
+        $redis->select(15);
 
-            // 计算时间:当前时间前 30 分钟
-            $time = date('Y-m-d H:i:s', strtotime('-30 minutes'));
+        $key = 'queues:txttoimg';
+        $key1 = 'queues:imgtotxt';
+        $count = $redis->lLen($key);
+        $count1 = $redis->lLen($key1);
+        $count = $count+$count1;
+        // 计算时间:当前时间前30分钟
+        $time = date('Y-m-d H:i:s', strtotime('-30 minutes'));
 
-            // 删除数据库中状态为0且在近30分钟的数据
+        if ($count === 0) {
+            // 删除数据库中状态为0且 created_at 在最近30分钟的数据
             $deleteCount = Db::name('queue_log')
-                ->where('status', 0)
                 ->where('created_at', '>=', $time)
                 ->delete();
-
-            // 如果 Redis 队列不为空,则清空
-            if ($count > 0) {
-                $redis->del($key);
-            }
-
-            // 尝试停止 systemd 队列服务
-            exec('sudo /bin/systemctl stop think-queue.service', $output, $status);
-
-            if ($status === 0) {
-                return json([
-                    'code' => 0,
-                    'msg'  => "队列服务已成功停止"
-                ]);
-            } else {
-                return json([
-                    'code' => 2,
-                    'msg'  => "但服务停止失败,请检查权限"
-                ]);
-            }
-        } catch (\Exception $e) {
             return json([
-                'code' => 500,
-                'msg'  => '处理异常:' . $e->getMessage()
+                'code' => 1,
+                'msg'  => '暂无队列需要停止'
             ]);
         }
-    }
-//    public function stopQueueProcesses()
-//    {
-//        $redis = new \Redis();
-//        $redis->connect('127.0.0.1', 6379);
-//        $redis->select(15);
-//
-//        $key = 'queues:default';
-//        $count = $redis->lLen($key);
-//        // 计算时间:当前时间前30分钟
-//        $time = date('Y-m-d H:i:s', strtotime('-30 minutes'));
-//
-//        if ($count === 0) {
-//            // 删除数据库中状态为0且 created_at 在最近30分钟的数据
-//            $deleteCount = Db::name('queue_log')
-//                ->where('created_at', '>=', $time)
-//                ->delete();
-//            return json([
-//                'code' => 1,
-//                'msg'  => '暂无队列需要停止'
-//            ]);
-//        }
-//
-//        // 清空 Redis 队列
-//        $redis->del($key);
-//
-//
-//
-//        // 删除数据库中状态为0且 created_at 在最近30分钟的数据
-//        $deleteCount = Db::name('queue_log')
-//            ->where('created_at', '>=', $time)
-//            ->delete();
-//
-//        return json([
-//            'code' => 0,
-//            'msg'  => '已成功停止队列任务'
-//        ]);
-//    }
+
+        // 清空 Redis 队列
+        $redis->del($key);
+        $redis->del($key1);
 
 
+        // 删除数据库中状态为0且 created_at 在最近30分钟的数据
+        $deleteCount = Db::name('queue_log')
+            ->where('created_at', '>=', $time)
+            ->delete();
+
+        return json([
+            'code' => 0,
+            'msg'  => '已成功停止队列任务'
+        ]);
+    }
     /**
      * 显示当前运行中的队列监听进程
      */
@@ -202,7 +158,7 @@ class WorkOrder extends Api
         $redis = new \Redis();
         $redis->connect('127.0.0.1', 6379);
         $redis->select(15);
-        $key = 'queues:default';
+        $key = 'queues:imgtotxt';
         $count = $redis->lLen($key);
         $list = $redis->lRange($key, 0, 9);
         $parsed = array_map(function ($item) {
@@ -217,6 +173,17 @@ class WorkOrder extends Api
     }
 
 
+    //单个调用[可以用来做测试]
+//    protected $config = [
+//        'gpt' => [
+//            'api_key' => 'sk-Bhos1lXTRpZiAAmN06624a219a874eCd91Dc068b902a3e73',
+//            'api_url' => 'https://one.opengptgod.com/v1/chat/completions'
+//        ],
+//        'dalle' => [
+//            'api_key' => 'sk-e0JuPjMntkbgi1BoMjrqyyzMKzAxILkQzyGMSy3xiMupuoWY',
+//            'api_url' => 'https://niubi.zeabur.app/v1/images/generations'
+//        ]
+//    ];
 //    public function imageToTexts()
 //    {
 //        $params = $this->request->param();
@@ -500,58 +467,13 @@ class WorkOrder extends Api
 ////            return json(['code' => 1, 'msg' => '文生图失败:' . $e->getMessage()]);
 ////        }
 //    }
-
-
-
-    protected $config = [
-        'gpt' => [
-            'api_key' => 'sk-Bhos1lXTRpZiAAmN06624a219a874eCd91Dc068b902a3e73',
-            'api_url' => 'https://one.opengptgod.com/v1/chat/completions'
-        ],
-        'dalle' => [
-            'api_key' => 'sk-e0JuPjMntkbgi1BoMjrqyyzMKzAxILkQzyGMSy3xiMupuoWY',
-            'api_url' => 'https://niubi.zeabur.app/v1/images/generations'
-        ]
-    ];
-
-
-    public function callDallesssss()
-    {
-        // 确保目录存在
-        $rootPath = str_replace('\\', '/', ROOT_PATH);
-        $imgDir = rtrim($rootPath . 'public/' . '/uploads/img/', '/') . '/';
-        if (!is_dir($imgDir)) mkdir($imgDir, 0755, true);
-
-        $filename = 'dalle_' . date('Ymd_His') . '_' . rand(1000, 9999) . '.png';
-        $savePath = $imgDir . $filename;
-
-        $prompt = "这幅图案呈现了一棵精致的树木,树干与枝条采用金色设计,树上盛开着纯白色的花朵。花瓣层层叠加,呈现出优雅的立体感,花心部分呈现细腻的黄色。树叶也以金色为主,整体色调偏向温暖的金白色,背景简洁纯净,整体给人一种高雅且现代的艺术感";
-        $response = $this->callDalleApi($prompt);
-        if (!isset($response['data'][0]['url'])) {
-            throw new \Exception("图像生成失败,未返回图片链接。返回内容:" . json_encode($response));
-        }
-        $imageUrl = $response['data'][0]['url'];
-        // 下载图片到本地目录
-        $imgContent = file_get_contents($imageUrl);
-        if ($imgContent === false) {
-            throw new \Exception("无法下载生成的图像:{$imageUrl}");
-        }
-        file_put_contents($savePath, $imgContent);
-        // 日志记录
-        $logDir = $rootPath . 'runtime/logs/';
-        if (!is_dir($logDir)) mkdir($logDir, 0755, true);
-        file_put_contents($logDir . 'prompt_log.txt', date('Y-m-d H:i:s') . " prompt: {$prompt}\nURL: {$imageUrl}\n", FILE_APPEND);
-        echo "图像生成成功:public/uploads/img/{$filename}\n";
-    }
-
-
 //
 //    /**
 //     * 调用 DALL·E 接口
 //     * 文生图
 //     */
-    public function callDalleApi($prompt)
-    {
+//    public function callDalleApi($prompt)
+//    {
 //        $data = [
 //            'prompt' => $prompt,
 //            'model'  => 'dall-e-2',
@@ -559,67 +481,82 @@ class WorkOrder extends Api
 //            'size'   => '1024x1024'
 //        ];
 //        return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
-        $data = [
-            'prompt' => $prompt,
-            'model'   => 'dall-e-2',
-//            'model'   => 'gpt-image-1',
-            'n'       => 1,
-            'size'    => '1024x1024',
-            'quality' => 'standard',
-            'style'   => 'vivid',
-            'response_format' => 'url'
-        ];
-        return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
-    }
-
-    /**
-     * 通用API调用方法
-     */
-    public function callApi($url, $apiKey, $data)
-    {
-        $maxRetries = 2;
-        $attempt = 0;
-        $lastError = '';
-
-        while ($attempt <= $maxRetries) {
-            $ch = curl_init();
-            curl_setopt_array($ch, [
-                CURLOPT_URL => $url,
-                CURLOPT_RETURNTRANSFER => true,
-                CURLOPT_POST => true,
-                CURLOPT_POSTFIELDS => json_encode($data),
-                CURLOPT_HTTPHEADER => [
-                    'Content-Type: application/json',
-                    'Authorization: Bearer ' . $apiKey
-                ],
-                CURLOPT_TIMEOUT => 120,
-                CURLOPT_SSL_VERIFYPEER => false,
-                CURLOPT_SSL_VERIFYHOST => 0,
-                CURLOPT_TCP_KEEPALIVE => 1,
-                CURLOPT_FORBID_REUSE => false
-            ]);
-
-            $response = curl_exec($ch);
-            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-            $curlError = curl_error($ch);
-            curl_close($ch);
-
-            if ($response !== false && $httpCode === 200) {
-                $result = json_decode($response, true);
-                return $result;
-            }
-
-            $lastError = $curlError ?: "HTTP错误:{$httpCode}";
-            $attempt++;
-            sleep(1);
-        }
-
-        throw new \Exception("请求失败(重试{$maxRetries}次):{$lastError}");
-    }
-
-
-
-
+////        $data = [
+////            'prompt'  => "A stylized representation of a Dallas football team logo, featuring a helmet in shades of gray and white with blue and black accents. The word 'Dallas' in bold, italicized, gray-white capital letters on a dark blue curved banner, with the year '1960' in smaller font at the bottom, matching the helmet's color scheme. The design reflects the visual elements and style typical of American football culture, presented on a plain black background.",
+////            'model'   => 'dall-e-2',
+////            'n'       => 1,
+////            'size'    => '1024x1024',
+////            'quality' => 'standard',
+////            'style'   => 'vivid'
+////        ];
+////
+////        return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
+//    }
+//
+//    /**
+//     * 翻译为英文
+//     */
+//    public function translateToEnglish($text)
+//    {
+//        $data = [
+//            'model' => 'gpt-3.5-turbo',
+//            'messages' => [[
+//                'role' => 'user',
+//                'content' => "请将以下内容翻译为英文,仅输出英文翻译内容,不需要解释:\n\n{$text}"
+//            ]],
+//            'max_tokens' => 300,
+//            'temperature' => 0.3
+//        ];
+//
+//        $response = $this->callApi($this->config['gpt']['api_url'], $this->config['gpt']['api_key'], $data);
+//        return trim($response['choices'][0]['message']['content'] ?? '');
+//    }
+//
+//
+//    /**
+//     * 通用API调用方法
+//     */
+//    public function callApi($url, $apiKey, $data)
+//    {
+//        $maxRetries = 2;
+//        $attempt = 0;
+//        $lastError = '';
+//
+//        while ($attempt <= $maxRetries) {
+//            $ch = curl_init();
+//            curl_setopt_array($ch, [
+//                CURLOPT_URL => $url,
+//                CURLOPT_RETURNTRANSFER => true,
+//                CURLOPT_POST => true,
+//                CURLOPT_POSTFIELDS => json_encode($data),
+//                CURLOPT_HTTPHEADER => [
+//                    'Content-Type: application/json',
+//                    'Authorization: Bearer ' . $apiKey
+//                ],
+//                CURLOPT_TIMEOUT => 120,
+//                CURLOPT_SSL_VERIFYPEER => false,
+//                CURLOPT_SSL_VERIFYHOST => 0,
+//                CURLOPT_TCP_KEEPALIVE => 1,
+//                CURLOPT_FORBID_REUSE => false
+//            ]);
+//
+//            $response = curl_exec($ch);
+//            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+//            $curlError = curl_error($ch);
+//            curl_close($ch);
+//
+//            if ($response !== false && $httpCode === 200) {
+//                $result = json_decode($response, true);
+//                return $result;
+//            }
+//
+//            $lastError = $curlError ?: "HTTP错误:{$httpCode}";
+//            $attempt++;
+//            sleep(1);
+//        }
+//
+//        throw new \Exception("请求失败(重试{$maxRetries}次):{$lastError}");
+//    }
 //
 //    /**
 //     * 记录到数据库

+ 39 - 0
application/job/ImageArrJob.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace app\job;
+use think\Db;
+use think\queue\Job;
+use think\Queue;
+class ImageArrJob
+{
+    public function fire(Job $job, $data)
+    {
+        echo "批量推送任务已开始\n";
+//        echo "接收的数据: " . json_encode($data) . "\n";
+        //任务分解
+        foreach ($data as $key => $value){
+            // 推送队列前,记录日志
+            $id = Db::name('queue_log')->insertGetId([
+                'job_name' => 'app\job\ImageJob',
+                'status' => 0,
+                'data' => json_encode($value, JSON_UNESCAPED_UNICODE),
+                'created_at' => date('Y-m-d H:i:s'),
+            ]);
+
+            // 将日志ID传入队列中
+            $value['log_id'] = $id;
+            Queue::push('app\job\ImageJob', $value,"imgtotxt");
+        }
+        echo "批量推送任务结束\n";
+        $job->delete();
+    }
+
+    /**
+     * 任务失败时的处理
+     */
+    public function failed($data)
+    {
+        // 记录失败日志或发送通知
+        \think\Log::error("ImageJob failed: " . json_encode($data));
+    }
+}

+ 110 - 181
application/job/ImageJob.php

@@ -4,6 +4,7 @@ namespace app\job;
 
 use think\Db;
 use think\queue\Job;
+use think\Queue;
 
 class ImageJob
 {
@@ -17,6 +18,7 @@ class ImageJob
             'api_url' => 'https://niubi.zeabur.app/v1/images/generations'
         ]
     ];
+
     /**
      * fire方法是队列默认调用的方法
      * @param Job $job 当前的任务对象
@@ -24,11 +26,12 @@ class ImageJob
      */
     public function fire(Job $job, $data)
     {
-        echo "队列任务开始执行\n";
-        echo "接收的数据: " . json_encode($data) . "\n";
+        echo "图生文开始\n";
+//        echo "接收的数据: " . json_encode($data) . "\n";
 
         $logId = $data['log_id'] ?? null;
         try {
+            // 如果有 log_id,更新任务状态为“正在处理”
             if ($logId) {
                 Db::name('queue_log')->where('id', $logId)->update([
                     'status' => 1, // 正在处理
@@ -37,69 +40,36 @@ class ImageJob
             }
 
             // 执行业务逻辑
-            $this->processImage($data);
-
-            if ($logId) {
-                Db::name('queue_log')->where('id', $logId)->update([
-                    'status' => 2,
-                    'log' => '执行成功',
-                    'updated_at' => date('Y-m-d H:i:s')
-                ]);
-            }
-
+            $str = $this->processImage($data);
+            echo $str;
+            echo "图生文结束\n";
             $job->delete();
         } catch (\Exception $e) {
+            // 如果有 log_id,更新任务状态为“执行失败”并记录错误信息
             if ($logId) {
                 Db::name('queue_log')->where('id', $logId)->update([
-                    'status' => 3,
+                    'status' => 3, // 执行失败
                     'log' => $e->getMessage(),
                     'updated_at' => date('Y-m-d H:i:s')
                 ]);
             }
 
-            // 重试
-            if ($job->attempts() < 3) {
-                $job->release(30);
+            // 最多重试一次(总执行两次)
+            if ($job->attempts() < 2) {
+                $job->release(30); // 延迟30秒再次执行
             } else {
-                $job->failed();
+                $job->failed(); // 达到最大尝试次数,标记失败
             }
         }
     }
 
-//    public function fire(Job $job, $data)
-//    {
-//        echo "队列任务开始执行\n";
-//        echo "接收的数据: " . json_encode($data) . "\n";
-//
-//        try {
-//            // 执行实际的业务逻辑
-//            $this->processImage($data);
-//
-//            // 任务执行成功后删除
-//            $job->delete();
-//            echo "任务执行成wwww功\n";
-//
-//        } catch (\Exception $e) {
-//            echo "任务执行失败: " . $e->getMessage() . "\n";
-//
-//            // 重试机制
-//            if ($job->attempts() < 3) {
-//                $job->release(30); // 30秒后重试
-//                echo "任务重新入队,重试次数: " . $job->attempts() . "\n";
-//            } else {
-//                $job->failed();
-//                echo "任务最终失败\n";
-//            }
-//        }
-//    }
-
     /**
      * 任务失败时的处理
      */
     public function failed($data)
     {
         // 记录失败日志或发送通知
-        \think\Log::error("ImageJob failed: " . json_encode($data));
+        echo "ImageJob failed: " . json_encode($data);
     }
 
     /**
@@ -112,8 +82,13 @@ class ImageJob
         echo $res;
     }
 
-    public function imageToText($sourceDirRaw,$fileName,$prompt,$call_data)
+
+    /**
+     * 图生文接口
+     */
+    public function imageToText($sourceDirRaw, $fileName, $prompt, $call_data)
     {
+
         // 自动拆分文件名
         if (!$fileName && preg_match('/([^\/]+\.(jpg|jpeg|png))$/i', $sourceDirRaw, $matches)) {
             $fileName = $matches[1];
@@ -124,6 +99,7 @@ class ImageJob
         if ($sourceDirRaw === '' || $fileName === '') {
             return '参数错误:原图路径 或 图片名称 不能为空';
         }
+
         // 构建路径
         $rootPath = str_replace('\\', '/', ROOT_PATH);
         $sourceDir = rtrim($rootPath . 'public/' . $sourceDirRaw, '/') . '/';
@@ -138,7 +114,6 @@ class ImageJob
             return '文件不存在:' . $filePath;
         }
 
-
         // 获取图片信息
         $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
         $mime = ($ext === 'jpg' || $ext === 'jpeg') ? 'jpeg' : $ext;
@@ -150,163 +125,115 @@ class ImageJob
         }
         $imageUrl = "data:image/{$mime};base64,{$imageData}";
 
-        // 构建严格格式的提示词
-        //请严格按以下要求分析图案:只提取图案本身的视觉元素(图形、字母、文字、符号),忽略所有背景和载体信息(如壁画载体、衣服等), 描述必须包含: 主体图形特征(形状/颜色/材质感),文字内容(字母/单词/数字及其样式),空间排列关系,整体艺术风格---json json--- 格式:{纯图案的客观中文描述,不包含任何背景说明}---json json---{ "prompt": "English description focusing only on graphic elements with style details, on pure black background","size": "1024x1024","color_palette": ["主色1", "主色2"],"style": "图案风格"}---json json---
-        $userPrompt = preg_replace('/\s+/u', '', $prompt); // 移除所有空白字符
-        $strictPrompt = "严格遵守以下规则:
-                        1. 只返回三段内容:
-                           第一段:纯中文图案描述
-                           第二段:---json json---
-                           第三段:纯英文图案描述
-                        2. 描述中必须体现图案的类型、颜色、风格等关键信息
-                        3. 不允许添加任何解释、引导、说明、示例等文字,必须只包含图案描述内容本身
-                        4. 示例:
-                        这张图中的图案是代表达拉斯足球队的标志,包括一个头盔图形和围绕它的文字。头盔以灰色和白色为主,有蓝色和黑色的细节。
-                        ---json json---
-                        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.
-                        请直接描述这个图案:
-                        " . $userPrompt;
+        // 记录提示词日志
+        $logDir = $rootPath . 'runtime/logs/';
+        if (!is_dir($logDir)) mkdir($logDir, 0755, true);
 
+        // file_put_contents(
+        //     $logDir . 'text.txt',
+        //     "\n====原始 " . date('Y-m-d H:i:s') . " ====\n" . $prompt . "\n\n",
+        //     FILE_APPEND
+        // );
         // 调用图生文
-        $gptRes = $this->callGptApi($imageUrl, $strictPrompt);
+        $gptRes = $this->callGptApi($imageUrl, $prompt);
         $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
 
-        // 验证 GPT 返回格式
-        if (strpos($gptText, '---json json---') === false) {
-            return  'GPT 返回格式不正确,缺少分隔符';
-        }
 
-        list($chineseDesc, $englishDesc) = array_map('trim', explode('---json json---', $gptText));
+        // 保存 GPT 返回内容
+        // file_put_contents($logDir . 'text.txt',
+        //     "\n==== " . date('Y-m-d H:i:s') . " ====\n" . $gptText . "\n\n",
+        //     FILE_APPEND
+        // );
 
-        if ($chineseDesc === '' || $englishDesc === '') {
-            return  '描述内容为空,请检查 GPT 返回';
-        }
+// 提取英文描述
+        $patternEnglish = '/^([\s\S]+?)---json json---/';
+        preg_match($patternEnglish, $gptText, $matchEn);
+        $englishDesc = isset($matchEn[1]) ? trim($matchEn[1]) : '';
 
-        // 插入数据库(成功时才插入)
-        $this->logToDatabase([
-            'old_image_url'       => $relativePath,
-            'chinese_description' => $chineseDesc,
-            'english_description' => $englishDesc,
-            'size'                => "",
-            'status'              => 1
-        ]);
-        //进行文字转图片
-        $res = $this->textToImage($fileName,$call_data["outputDir"],$call_data["width"],$call_data["height"],$chineseDesc.$englishDesc);
-        return $res;
+// 提取中文描述
+        $patternChinese = '/---json json---\s*([\x{4e00}-\x{9fa5}][\s\S]+?)---json json---/u';
+        preg_match($patternChinese, $gptText, $matchZh);
+        $chineseDesc = isset($matchZh[1]) ? trim($matchZh[1]) : '';
 
-    }
+// 提取图片名(可能是中文短句,也可能是关键词)
+        $patternName = '/---json json---\s*(.+)$/s';
+        preg_match($patternName, $gptText, $matchName);
+        $rawName = isset($matchName[1]) ? trim($matchName[1]) : '';
+        $img_name = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9_\- ]/u', '', $rawName);
 
-    public function textToImage($fileName, $outputDirRaw, $width, $height, $prompt)
-    {
-        // 统一路径分隔符为 /
-        $rootPath = str_replace('\\', '/', ROOT_PATH);
-        $outputDir = rtrim($rootPath . 'public/' . $outputDirRaw, '/') . '/';
-        $dateDir = date('Y-m-d') . '/';
-        $fullBaseDir = $outputDir . $dateDir;
-
-        // 创建所需目录
-        foreach ([$fullBaseDir, $fullBaseDir . '1024x1024/', $fullBaseDir . "{$width}x{$height}/"] as $dir) {
-            if (!is_dir($dir)) {
-                mkdir($dir, 0755, true);
-            }
-        }
-
-        // 规范化提示词
-        $prompt = preg_replace('/[\r\n\t]+/', ' ', $prompt);
-
-        // 查询数据库记录
-        $record = Db::name('text_to_image')
-            ->where('old_image_url', 'like', "%{$fileName}")
-            ->order('id desc')
-            ->find();
-
-        if (!$record) {
-            return '没有找到匹配的图像记录';
-        }
-
-        // 记录提示词日志
-        $logDir = $rootPath . 'runtime/logs/';
-        if (!is_dir($logDir)) mkdir($logDir, 0755, true);
-        file_put_contents($logDir . 'prompt_log.txt', date('Y-m-d H:i:s') . " prompt: {$prompt}\n", FILE_APPEND);
-
-        // 调用 DALL·E 接口
-        $dalle1024 = $this->callDalleApi($prompt);
-        file_put_contents($logDir . 'dalle_response.log', date('Y-m-d H:i:s') . "\n" . print_r($dalle1024, true) . "\n", FILE_APPEND);
-
-        // 校验返回链接
-        if (!isset($dalle1024['data'][0]['url']) || empty($dalle1024['data'][0]['url'])) {
-            $errorText = $dalle1024['error']['message'] ?? '未知错误';
-            throw new \Exception('DALL·E 生成失败:' . $errorText);
-        }
-
-        $imgUrl1024 = $dalle1024['data'][0]['url'];
-        $imgData1024 = @file_get_contents($imgUrl1024);
-        if (!$imgData1024 || strlen($imgData1024) < 1000) {
-            return "下载图像失败或内容异常";
-        }
-
-        // 保存原图
-        $filename1024 = 'dalle_' . md5($record['old_image_url'] . microtime()) . '_1024.png';
-        $savePath1024 = $fullBaseDir . '1024x1024/' . $filename1024;
-        file_put_contents($savePath1024, $imgData1024);
+        // file_put_contents(
+        //     $logDir . 'text.txt',
+        //     "\n==== " . date('Y-m-d H:i:s') . " ====\n" . $gptText . "\n\n",
+        //     FILE_APPEND
+        // );
 
-        // 创建图像资源
-        $im = @imagecreatefromstring($imgData1024);
-        if (!$im) {
-            return "图像格式不受支持或已损坏";
+        // 验证 GPT 返回格式
+        if (strpos($gptText, '---json json---') === false) {
+            return  'GPT 返回格式不正确,缺少分隔符';
         }
 
-        // 获取原图尺寸
-        $srcWidth = imagesx($im);
-        $srcHeight = imagesy($im);
-
-        // 创建目标图像(缩放到目标尺寸,无裁剪)
-        $dstImg = imagecreatetruecolor($width, $height);
-        imagecopyresampled($dstImg, $im, 0, 0, 0, 0, $width, $height, $srcWidth, $srcHeight);
-
-        // 保存缩放图
-        $filenameCustom = 'dalle_' . md5($record['old_image_url'] . microtime()) . "_custom.png";
-        $savePathCustom = $fullBaseDir . "{$width}x{$height}/" . $filenameCustom;
-        imagepng($dstImg, $savePathCustom);
-
-        // 释放资源
-        imagedestroy($im);
-        imagedestroy($dstImg);
-
-        // 更新数据库
-        Db::name('text_to_image')->where('id', $record['id'])->update([
-            'new_image_url'     => str_replace($rootPath . 'public/', '', $savePath1024),
-            'custom_image_url'  => str_replace($rootPath . 'public/', '', $savePathCustom),
-            'error_msg'         => '',
-            'size'              => "{$width}x{$height}",
-            'updated_time'      => date('Y-m-d H:i:s')
+        // 以 ---json json--- 分割
+        $parts = array_map('trim', explode('---json json---', $gptText));
+
+        // 清理“第一段”、“第二段”等标签前缀
+        $cleanPrefix = function ($text) {
+            return preg_replace('/^第[一二三四五六七八九十]+段[::]?\s*/u', '', $text);
+        };
+        // 防止越界,逐个安全提取
+        $englishDesc = isset($parts[0]) ? $cleanPrefix(trim($parts[0])) : '';
+        $chineseDesc = isset($parts[1]) ? $cleanPrefix(trim($parts[1])) : '';
+        $part2 = isset($parts[2]) ? $cleanPrefix(trim($parts[2])) : '';
+
+        // 提取图片名
+        // 只保留中英文、数字、下划线、短横线、空格
+        $img_name = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9_\- ]/u', '', $part2);
+
+
+        // 成功后的日志
+        // file_put_contents(
+        //     $logDir . 'img_name_success.txt',
+        //     "\n======== " . date('Y-m-d H:i:s') . " ========\n" .
+        //     $englishDesc . "\n---json json---\n" .
+        //     $chineseDesc . "\n---json json---\n" .
+        //     $img_name . "\n\n",
+        //     FILE_APPEND
+        // );
+
+        // 成功写入数据库
+        $this->logToDatabase([
+            'img_name' => $img_name,
+            'old_image_url' => $relativePath,
+            'chinese_description' => $chineseDesc,
+            'english_description' => $englishDesc,
+            'size' => "",
+            'status' => 0 // 正常待图生图状态
         ]);
-
-        return 0;
-    }
-
-    public function callDalleApi($prompt)
-    {
-        $data = [
-            'prompt' => $prompt,
-            'model' => 'dall-e-2',
-            'n' => 1,
-            'size' => '1024x1024'
+        //分解任务
+        $arr = [
+            "fileName" =>$fileName,
+            "outputDir"=>$call_data["outputDir"],
+            "width"=>$call_data["width"],
+            "height"=>$call_data["height"],
+            "englishDesc"=>$englishDesc,
+            "img_name"=>$img_name
         ];
-        return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
-    }
+        echo "现在推送";
+        Queue::push('app\job\TextToImageJob', $arr,'txttoimg');
+        return ;
+        // 执行文生图
 
+    }
     public function logToDatabase($data)
     {
         $record = [
             'old_image_url' => $data['old_image_url'] ?? '',
             'new_image_url' => $data['new_image_url'] ?? '',
             'custom_image_url' => $data['custom_image_url'] ?? '',
-            'size' => isset($data['image_width'], $data['image_height']) ?
-                $data['image_width'] . 'x' . $data['image_height'] : '',
+            'img_name' => $data['img_name'],
+            'size' => isset($data['image_width'], $data['image_height']) ? $data['image_width'] . 'x' . $data['image_height'] : '',
             'chinese_description' => $data['chinese_description'] ?? '',
             'english_description' => $data['english_description'] ?? '',
-            'model' => 'dall-e-2',
+            'model' => 'gpt-image-1',
             'quality' => 'standard',
             'style' => 'vivid',
             'status' => $data['status'] ?? 0,
@@ -321,6 +248,7 @@ class ImageJob
             Db::name('text_to_image')->insert($record);
         }
     }
+
     public function callGptApi($imageUrl, $prompt)
     {
         $data = [
@@ -385,4 +313,5 @@ class ImageJob
 
         throw new \Exception("请求失败(重试{$maxRetries}次):{$lastError}");
     }
+
 }

+ 287 - 0
application/job/TextToImageJob.php

@@ -0,0 +1,287 @@
+<?php
+
+namespace app\job;
+
+use think\Db;
+use think\Queue;
+use think\queue\Job;
+
+class TextToImageJob
+{
+    protected $config = [
+        'gpt' => [
+            'api_key' => 'sk-Bhos1lXTRpZiAAmN06624a219a874eCd91Dc068b902a3e73',
+            'api_url' => 'https://one.opengptgod.com/v1/chat/completions'
+        ],
+        'dalle' => [
+            'api_key' => 'sk-e0JuPjMntkbgi1BoMjrqyyzMKzAxILkQzyGMSy3xiMupuoWY',
+            'api_url' => 'https://niubi.zeabur.app/v1/images/generations'
+        ]
+    ];
+
+    public function fire(Job $job, $data)
+    {
+        echo "已经到了文生图阶段\n";
+        try {
+            $str = $this->textToImage(
+                $data["fileName"],
+                $data["outputDir"],
+                $data["width"],
+                $data["height"],
+                $data["englishDesc"],
+                $data["img_name"]
+            );
+            echo $str;
+            echo "文生图结束\n";
+            $job->delete();
+        } catch (\Exception $e) {
+            echo "Error message: " . $e->getMessage() . "\n";
+            echo "Error file: " . $e->getFile() . "\n";
+            echo "Error line: " . $e->getLine() . "\n";
+            echo "Stack trace: " . $e->getTraceAsString() . "\n";
+            // 如果有 log_id,更新任务状态为“执行失败”并记录错误信息
+            // 最多重试一次(总执行两次)
+            if ($job->attempts() < 2) {
+                $job->release(30); // 延迟30秒再次执行
+            } else {
+                $job->failed(); // 达到最大尝试次数,标记失败
+            }
+        }
+    }
+    /**
+     * 任务失败时的处理
+     */
+    public function failed($data)
+    {
+        // 记录失败日志或发送通知
+        echo "ImageJob failed: " . json_encode($data);
+    }
+
+    /**
+     * 文生图接口
+     */
+    public function textToImage($fileName, $outputDirRaw, $width, $height, $prompt, $img_name)
+    {
+        $rootPath = str_replace('\\', '/', ROOT_PATH);
+        $outputDir = rtrim($rootPath . 'public/' . $outputDirRaw, '/') . '/';
+        $dateDir = date('Y-m-d') . '/';
+        $fullBaseDir = $outputDir . $dateDir;
+
+        // 创建输出目录结构
+        foreach ([$fullBaseDir, $fullBaseDir . '1024x1024/', $fullBaseDir . "{$width}x{$height}/"] as $dir) {
+            if (!is_dir($dir)) {
+                mkdir($dir, 0755, true);
+            }
+        }
+
+        // 清理 prompt 的换行
+        $prompt = preg_replace('/[\r\n\t]+/', ' ', $prompt);
+
+        // 查询数据库记录
+        $record = Db::name('text_to_image')
+            ->where('old_image_url', 'like', "%{$fileName}")
+            ->order('id desc')
+            ->find();
+
+        if (!$record) {
+            return '没有找到匹配的图像记录';
+        }
+
+        // 写入 prompt 日志
+        $logDir = $rootPath . 'runtime/logs/';
+        if (!is_dir($logDir)) mkdir($logDir, 0755, true);
+//        echo 2345;
+        // 调用文生图模型接口生成图像
+        $startTime = microtime(true);
+        $dalle1024 = $this->callDalleApi($prompt);
+        // $dalle1024 = json_decode('{"created":1747932746,"data":[{"revised_prompt":"**First paragraph:**   A geometric abstract design with a central motif consisting of curved and angular shapes in a symmetrical arrangement. The color scheme predominantly features shades of blue, with hints of white and black creating a contrast. The design incorporates smooth, flowing lines mixed with sharp angles. The overall style has a modern, minimalist aesthetic, with a focus on balance and clean shapes.","url":"https:\/\/filesystem.site\/cdn\/20250523\/3NVcCUaZDkLimWjtgOwJYniGezDX8d.png"}],"usage":{"total_tokens":4250,"input_tokens":75,"output_tokens":4175,"input_tokens_details":{"text_tokens":75}}}',true);
+
+        $endTime = microtime(true);
+        $executionTime = $endTime - $startTime;
+        echo "API调用耗时: " . round($executionTime, 3) . " 秒\n";
+
+        // 检查 URL 返回是否成功
+        if (!isset($dalle1024['data'][0]['url']) || empty($dalle1024['data'][0]['url'])) {
+            $errorText = $dalle1024['error']['message'] ?? '未知错误';
+            echo '生成失败:' . $errorText;
+        }
+//        echo 342342;
+        // 下载图像
+        $imgUrl1024 = $dalle1024['data'][0]['url'];
+        $imgData1024 = @file_get_contents($imgUrl1024);
+        if (!$imgData1024 || strlen($imgData1024) < 1000) {
+            return "下载图像失败或内容异常";
+        }
+//        echo 3423424444;
+        // file_put_contents(
+        //     $logDir . 'img_name.txt',
+        //     "\n====图片 " . date('Y-m-d H:i:s') . " ====\n" . $img_name . "\n\n",
+        //     FILE_APPEND
+        // );
+
+        // 保存原图(1024x1024)
+//        $img_name = $this->limitStringLength($img_name);
+        $img_name = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9_\- ]/u', '', $img_name);
+        $img_name = mb_substr($img_name, 0, 30); // 限制为前30个字符(避免路径过长)
+
+        $filename1024 = $img_name . '.png';
+        $savePath1024 = $fullBaseDir . '1024x1024/' . $filename1024;
+        file_put_contents($savePath1024, $imgData1024);
+//        echo 342344543532;
+        // 处理缩略图
+        $im = @imagecreatefromstring($imgData1024);
+        if (!$im) return "图像格式不受支持或已损坏";
+
+        $srcWidth = imagesx($im);
+        $srcHeight = imagesy($im);
+        $srcRatio = $srcWidth / $srcHeight;
+        $dstRatio = $width / $height;
+
+        // 居中裁剪逻辑
+        if ($srcRatio > $dstRatio) {
+            $cropHeight = $srcHeight;
+            $cropWidth = intval($srcHeight * $dstRatio);
+            $srcX = intval(($srcWidth - $cropWidth) / 2);
+            $srcY = 0;
+        } else {
+            $cropWidth = $srcWidth;
+            $cropHeight = intval($srcWidth / $dstRatio);
+            $srcX = 0;
+            $srcY = intval(($srcHeight - $cropHeight) / 2);
+        }
+//        echo 789;
+        $dstImg = imagecreatetruecolor($width, $height);
+        imagecopyresampled($dstImg, $im, 0, 0, $srcX, $srcY, $width, $height, $cropWidth, $cropHeight);
+
+        // 保存裁剪后图像
+        $filenameCustom = $img_name . ".png";
+        $savePathCustom = $fullBaseDir . "{$width}x{$height}/" . $filenameCustom;
+        imagepng($dstImg, $savePathCustom);
+        imagedestroy($im);
+        imagedestroy($dstImg);
+
+
+        // file_put_contents(
+        //     $logDir . 'image_url.txt',
+        //     "\n====图片路径 " . date('Y-m-d H:i:s') . " ====\n" . str_replace($rootPath . 'public/', '', $savePath1024) . "\n\n",
+        //     "\n====图片路径 " . date('Y-m-d H:i:s') . " ====\n" . str_replace($rootPath . 'public/', '', $savePathCustom) . "\n\n",
+        //     FILE_APPEND
+        // );
+        $status = trim($img_name) === '' ? 0 : 1;
+        // 更新数据库记录
+        $updateRes = Db::name('text_to_image')->where('id', $record['id'])->update([
+            'new_image_url' => str_replace($rootPath . 'public/', '', $savePath1024),
+            'custom_image_url' => str_replace($rootPath . 'public/', '', $savePathCustom),
+            'img_name' => $img_name,
+            'error_msg' => '',
+            'size' => "{$width}x{$height}",
+            'updated_time' => date('Y-m-d H:i:s'),
+            'status' => $status
+        ]);
+        return 0;
+    }
+
+    /**
+     * 处理字符串长度,超出限制则截断
+     *
+     * @param string $str 输入字符串
+     * @param int $maxLength 最大长度限制(默认200)
+     * @return string 处理后的字符串
+     */
+    public function limitStringLength($str, $maxLength = 10)
+    {
+        // 如果字符串长度没有超出限制,直接返回
+        if (mb_strlen($str, 'UTF-8') <= $maxLength) {
+            return $str;
+        }
+
+        // 超出限制则截断
+        return mb_substr($str, 0, $maxLength, 'UTF-8');
+    }
+    public function cleanImageUrl($input) {
+        // 去除字符串首尾空格和中文引号替换为英文引号
+        $input = trim($input);
+        $input = str_replace(['“', '”', '‘', '’'], '"', $input);
+
+        // 判断是否为纯中文文字
+        if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $input)) {
+            // 纯中文:替换掉不适合用于文件名的字符
+            $cleaned = preg_replace('/[\/\\\:\*\?"<>\|,。!¥【】、;‘’“”《》\s]+/u', '', $input);
+        } elseif (preg_match('/[a-zA-Z]/', $input) && !preg_match('/[\x{4e00}-\x{9fa5}]/u', $input)) {
+            // 如果是纯字母和空格,且没有中文字符:保留空格,去掉其他符号
+            $cleaned = preg_replace('/[^a-zA-Z\s]/', '', $input);
+        } else {
+            // 如果包含中文或是其他混合字符,按照纯中文的规则清理符号
+            $cleaned = preg_replace('/[\/\\\:\*\?"<>\|,。!¥【】、;‘’“”《》\s]+/u', '', $input);
+        }
+
+        return $cleaned;
+    }
+
+
+    /**
+     * 文生图模型
+     */
+    public function callDalleApi($prompt)
+    {
+        $data = [
+            'prompt' => $prompt,
+//            'model'   => 'dall-e-2',
+            'model'   => 'gpt-image-1',
+            'n'       => 1,
+            'size'    => '1024x1024',
+            'quality' => 'standard',
+            'style'   => 'vivid',
+            'response_format' => 'url'
+        ];
+        return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
+    }
+
+
+
+
+    /**
+     * 通用API调用方法
+     */
+    public function callApi($url, $apiKey, $data)
+    {
+        $maxRetries = 2;
+        $attempt = 0;
+        $lastError = '';
+
+        while ($attempt <= $maxRetries) {
+            $ch = curl_init();
+            curl_setopt_array($ch, [
+                CURLOPT_URL => $url,
+                CURLOPT_RETURNTRANSFER => true,
+                CURLOPT_POST => true,
+                CURLOPT_POSTFIELDS => json_encode($data),
+                CURLOPT_HTTPHEADER => [
+                    'Content-Type: application/json',
+                    'Authorization: Bearer ' . $apiKey
+                ],
+                CURLOPT_TIMEOUT => 120,
+                CURLOPT_SSL_VERIFYPEER => false,
+                CURLOPT_SSL_VERIFYHOST => 0,
+                CURLOPT_TCP_KEEPALIVE => 1,
+                CURLOPT_FORBID_REUSE => false
+            ]);
+
+            $response = curl_exec($ch);
+            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+            $curlError = curl_error($ch);
+            curl_close($ch);
+
+            if ($response !== false && $httpCode === 200) {
+                $result = json_decode($response, true);
+                return $result;
+            }
+
+            $lastError = $curlError ?: "HTTP错误:{$httpCode}";
+            $attempt++;
+            sleep(1);
+        }
+
+        throw new \Exception("请求失败(重试{$maxRetries}次):{$lastError}");
+    }
+}

+ 63 - 25
application/service/ImageService.php

@@ -9,39 +9,77 @@ class ImageService
 {
     public function handleImage($params) {
         // 如果是单条,转为数组
-        if (isset($params['sourceDir'])) {
-            $params = [$params];
+        if(!isset($params["batch"])){
+            return false;
         }
         $arr = [];
-        foreach ($params as $k => $v) {
-            // 确保每项是数组,防止异常
-            if (!is_array($v)) continue;
-            $arr[$k] = [
-                "sourceDir" => $v['sourceDir'] ?? '',
-                "outputDir" => $v['outputDir'] ?? '',
-                "file_name" => $v['file_name'] ?? '',
-                "prompt" => $v['prompt'] ?? '',
-                "width" => $v['width'] ?? 512,
-                "height" => $v['height'] ?? 512
+        $batch = $params["batch"];
+        $num = $params["num"];
+        //进行数据处理
+        $j = 0;
+        foreach ($batch as $k => $v) {
+            $baseItem = [
+                "sourceDir" => $this->sourceDir($v, 1) ?? '',
+                "outputDir" => $this->sourceDir($v, 2) ?? '',
+                "file_name" => $this->sourceDir($v, 3) ?? '',
+                "prompt" => $params['prompt'] ?? '',
+                "width" => $params['width'] ?? 512,
+                "height" => $params['height'] ?? 512
             ];
+
+            // 创建$num个相同的项目并合并到$arr
+            $arr = array_merge($arr, array_fill(0, $num, $baseItem));
         }
 //         推送队列(启用时请去掉 die)
-         foreach ($arr as $value) {
-             // 推送队列前,记录日志
-             $id = Db::name('queue_log')->insertGetId([
-                 'job_name' => 'app\job\ImageJob',
-                 'status' => 0,
-                 'data' => json_encode($value, JSON_UNESCAPED_UNICODE),
-                 'created_at' => date('Y-m-d H:i:s'),
-             ]);
+        //整体进行推送
+        Queue::push('app\job\ImageArrJob', $arr,"arrimage");
+    }
+
+    public function sourceDir($filePath,$type){
+        $arr = [];
+        // 使用正则表达式匹配完整路径
+        if (preg_match('/^(.+?)\/Preview\/(\d{8})\/(.+)$/', $filePath, $matches)) {
+            $arr =  [
+                'basePath' => $matches[1],  // uploads/operate/ai
+                'date' => $matches[2],      // 20250522
+                'filename' => $matches[3]   // 333 (89).jpg
+            ];
+        }else{
+            // 备用方案:如果正则匹配失败
+            $pathParts = explode('/', $filePath);
+            $filename = array_pop($pathParts);
+
+            $date = '';
+            $baseParts = [];
 
-             // 将日志ID传入队列中
-             $value['log_id'] = $id;
+            foreach ($pathParts as $part) {
+                if (preg_match('/^\d{8}$/', $part)) {
+                    $date = $part;
+                    break;
+                }
+                $baseParts[] = $part;
+            }
+
+            $arr = [
+                'basePath' => implode('/', $baseParts),
+                'date' => $date,
+                'filename' => $filename
+            ];
+        }
 
-             // 推送到队列
-             Queue::push('app\job\ImageJob', $value);
-         }
 
 
+        if($type==1){
+            // 使用正则表达式匹配路径中的日期部分(YYYYMMDD)
+            return $arr["basePath"]."/Preview/".$arr["date"];
+        }
+        if($type==2){
+            // 使用正则表达式匹配路径中的日期部分(YYYYMMDD)
+            return '/'.$arr["basePath"]."/dall-e/".$arr["date"];
+        }
+        if($type==3){
+            // 使用正则表达式匹配路径中的日期部分(YYYYMMDD)
+            return $arr["filename"];
+        }
     }
 }