liuhairui 6 сар өмнө
parent
commit
1db13bf837

+ 353 - 381
application/api/controller/Facility.php

@@ -3,75 +3,16 @@ namespace app\api\controller;
 use app\common\controller\Api;
 use think\Db;
 use think\Request;
-
+use RecursiveIteratorIterator;
+use RecursiveDirectoryIterator;
 class Facility extends Api
 {
     protected $noNeedLogin = ['*'];
     protected $noNeedRight = ['*'];
 
-    public function packImagess()
-    {
-        try {
-            $params = $this->request->post();
-            $paths = $params['paths'] ?? [];
-
-            if (empty($paths) || !is_array($paths)) {
-                return json(['code' => 1, 'msg' => '路径参数不能为空或格式不正确']);
-            }
-
-            // 设置基础路径和压缩目录路径
-            $basePath = ROOT_PATH . 'public/';
-            $zipDir = $basePath . 'uploads/operate/ai/zip/';
-
-            if (!is_dir($zipDir)) {
-                mkdir($zipDir, 0755, true);
-            }
-
-            // 压缩包文件名及完整路径
-            $fileName = 'images_' . date('Ymd_His') . '.zip';
-            $zipPath = $zipDir . $fileName;
-
-            // 创建 Zip 文件
-            $zip = new \ZipArchive();
-            if ($zip->open($zipPath, \ZipArchive::CREATE) !== TRUE) {
-                return json(['code' => 1, 'msg' => '无法创建压缩包']);
-            }
-
-            // 添加文件到压缩包
-            $addCount = 0;
-            foreach ($paths as $relativePath) {
-                $relativePath = ltrim($relativePath, '/');
-                $fullPath = $basePath . $relativePath;
-
-                if (file_exists($fullPath)) {
-                    $zip->addFile($fullPath, basename($fullPath)); // 仅保存文件名
-                    $addCount++;
-                }
-            }
-
-            $zip->close();
-
-            if ($addCount === 0) {
-                return json(['code' => 1, 'msg' => '未找到有效图片,未生成压缩包']);
-            }
-
-            // 返回下载地址(注意路径与保存路径一致)
-            $downloadUrl = request()->domain() . '/uploads/operate/ai/zip/' . $fileName;
-
-            return json([
-                'code' => 0,
-                'msg' => '打包成功',
-                'download_url' => $downloadUrl
-            ]);
-        } catch (\Exception $e) {
-            return json([
-                'code' => 1,
-                'msg' => '异常错误:' . $e->getMessage()
-            ]);
-        }
-    }
-
-
+    /**
+     * 获取一张原目录图片对应明细数据
+     */
     public function getlsit()
     {
         // 获取前端传入的图片路径参数
@@ -87,12 +28,15 @@ class Facility extends Api
     }
 
 
-    //获取指定目录所有图片
+    /**
+     * 获取指定目录所有图片
+     */
     public function getPreviewimg()
     {
         $page = (int)$this->request->param('page', 1);
         $limit = (int)$this->request->param('limit', 50);
         $status = $this->request->param('status', '');
+        $status_name = $this->request->param('status_name', '');
         $relativePath = $this->request->param('path', '');
 
         $basePath = ROOT_PATH . 'public/';
@@ -102,66 +46,124 @@ class Facility extends Api
             return json(['code' => 1, 'msg' => '原图目录不存在']);
         }
 
-        // 1. 获取所有图片路径
-        $allImages = glob($fullPath . '/*.{jpg,jpeg,png}', GLOB_BRACE);
-        if (empty($allImages)) {
-            return json(['code' => 0, 'msg' => '暂无图片', 'data' => [], 'total' => 0]);
+        // 设置缓存路径
+        $cacheDir = RUNTIME_PATH . 'image_cache/';
+        if (!is_dir($cacheDir)) {
+            mkdir($cacheDir, 0755, true);
         }
-
-        // 2. 构建路径信息映射(路径 => 文件信息)
-        $imageInfoMap = [];
-        foreach ($allImages as $imgPath) {
-            $relative = str_replace('\\', '/', trim(str_replace($basePath, '', $imgPath), '/'));
-            $info = @getimagesize($imgPath);
-            $imageInfoMap[$relative] = [
-                'width' => $info[0] ?? 0,
-                'height' => $info[1] ?? 0,
-                'size_kb' => round(filesize($imgPath) / 1024, 2),
-                'created_time' => date('Y-m-d H:i:s', filectime($imgPath))
-            ];
+        $cacheFile = $cacheDir . md5($relativePath) . '.json';
+
+        // 判断缓存文件是否存在,并且最后修改时间在1小时(3600秒)以内
+        if (file_exists($cacheFile) && time() - filemtime($cacheFile) < 3600) {
+            $imageInfoMap = json_decode(file_get_contents($cacheFile), true);
+        } else {
+            // 没有缓存或缓存过期,重新扫描目录
+            $allImages = glob($fullPath . '/*.{jpg,jpeg,png}', GLOB_BRACE);
+            $imageInfoMap = [];
+            foreach ($allImages as $imgPath) {
+                $relative = str_replace('\\', '/', trim(str_replace($basePath, '', $imgPath), '/'));
+                $info = @getimagesize($imgPath);
+                $imageInfoMap[$relative] = [
+                    'width' => $info[0] ?? 0,
+                    'height' => $info[1] ?? 0,
+                    'size_kb' => round(filesize($imgPath) / 1024, 2),
+                    'created_time' => date('Y-m-d H:i:s', filectime($imgPath))
+                ];
+            }
+            file_put_contents($cacheFile, json_encode($imageInfoMap));
         }
 
+        // 1. 获取所有图片路径
         $relativeImages = array_keys($imageInfoMap);
 
-        // 3. 获取数据库记录
+        // 2. 查询数据库记录(一次性查询所有相关记录)
         $dbRecords = Db::name('text_to_image')
             ->whereIn('old_image_url', $relativeImages)
-            ->field('id as img_id, old_image_url, new_image_url, custom_image_url, chinese_description, english_description, img_name, status')
+            ->field('id as img_id, old_image_url, new_image_url, custom_image_url, chinese_description, english_description, img_name, status, status_name')
+            ->select();
+
+        // 3. 查询队列表中的记录(获取队列状态信息)
+        $queueRecords = Db::name('image_task_log')
+            // ->where('status', 0)
+            // ->where('log', '队列中')
+            ->field('file_name, log')
             ->select();
 
-        // 4. 构建映射表:路径 => 整条数据库记录
+        // 4. 创建队列信息映射
+        $queueMap = [];
+        foreach ($queueRecords as $queueItem) {
+            $key = str_replace('\\', '/', trim($queueItem['file_name'], '/'));
+            $queueMap[$key] = $queueItem['log'];
+        }
+
+        // 5. 映射记录
         $processedMap = [];
         foreach ($dbRecords as $item) {
             $key = str_replace('\\', '/', trim($item['old_image_url'], '/'));
             $processedMap[$key] = $item;
         }
 
-        // 5. 获取 same_count 统计
-        $sameCountMap = Db::name('text_to_image')
-            ->whereIn('old_image_url', $relativeImages)
-            ->where('status', 1)
-            ->group('old_image_url')
-            ->column('count(*) as cnt', 'old_image_url');
-
-        // 6. 构建结果数据
-        $allData = [];
+        // 6. 构建完整数据并进行筛选
+        $filteredData = [];
         foreach ($relativeImages as $path) {
             $item = $processedMap[$path] ?? [];
             $info = $imageInfoMap[$path];
 
-            $isProcessed = !empty($item['img_name']) && !empty($item['custom_image_url']);
             $dbStatus = isset($item['status']) ? (int)$item['status'] : 0;
+            $dbStatusName = isset($item['status_name']) ? trim($item['status_name']) : '';
 
-            // 状态过滤(0:未出图,1:已出图)
+            // 状态筛选条件
             if ($status !== '' && (int)$status !== $dbStatus) {
                 continue;
             }
+            if ($status_name !== '' && $dbStatusName !== $status_name) {
+                continue;
+            }
+
+            $isProcessed = !empty($item['img_name']) && !empty($item['custom_image_url']);
+            $queueStatus = $queueMap[$path] ?? '';
+
+            $filteredData[] = [
+                'path' => $path,
+                'item' => $item,
+                'info' => $info,
+                'dbStatus' => $dbStatus,
+                'dbStatusName' => $dbStatusName,
+                'isProcessed' => $isProcessed,
+                'queueStatus' => $queueStatus
+            ];
+        }
+
+        // 7. 获取相同图片数量统计(基于筛选后的结果,只统计有new_image_url的记录)
+        $filteredPaths = array_column($filteredData, 'path');
+        $sameCountMap = [];
+        if (!empty($filteredPaths)) {
+            $sameCountMap = Db::name('text_to_image')
+                ->whereIn('old_image_url', $filteredPaths)
+                ->where('new_image_url', '<>', '')  // 只统计有new_image_url的记录
+                ->group('old_image_url')
+                ->column('count(*) as cnt', 'old_image_url');
+        }
 
-            $allData[] = [
-                'path' => $path,//原图路径
-                'status' => $dbStatus,//状态
-                'same_count' => $sameCountMap[$path] ?? 0, // 出图数量
-                'is_processed' => $isProcessed ? 1 : 0,
+        // 8. 分页处理
+        $total = count($filteredData);
+        $pagedData = array_slice($filteredData, ($page - 1) * $limit, $limit);
+
+        // 9. 构建最终响应数据
+        $resultData = [];
+        foreach ($pagedData as $i => $data) {
+            $path = $data['path'];
+            $item = $data['item'];
+            $info = $data['info'];
+
+            $resultData[] = [
+                'id' => ($page - 1) * $limit + $i + 1,
+                'path' => $path,
+                'status' => $data['dbStatus'],
+                'status_name' => $data['dbStatusName'],
+                'same_count' => $sameCountMap[$path] ?? 0,
+                'is_processed' => $data['isProcessed'] ? 1 : 0,
+                'queue_status' => $data['queueStatus'],  // 新增队列状态字段
                 'new_image_url' => $item['new_image_url'] ?? '',
                 'custom_image_url' => $item['custom_image_url'] ?? '',
                 'chinese_description' => $item['chinese_description'] ?? '',
@@ -174,252 +176,67 @@ class Facility extends Api
             ];
         }
 
-        // 7. 分页处理
-        $total = count($allData);
-        $pagedData = array_slice(array_values($allData), ($page - 1) * $limit, $limit);
-        foreach ($pagedData as $i => &$row) {
-            $row['id'] = ($page - 1) * $limit + $i + 1;
-        }
-
         return json([
             'code' => 0,
             'msg' => '获取成功',
-            'data' => $pagedData,
+            'data' => $resultData,
             'total' => $total,
             'page' => $page,
             'limit' => $limit
         ]);
     }
 
-
-
-
-//    public function getPreviewimg()
-//    {
-//        $page = (int)$this->request->param('page', 1);
-//        $limit = (int)$this->request->param('limit', 50);
-//        $status = $this->request->param('status', '');
-//        $relativePath = $this->request->param('path', '');
-//
-//        $basePath = ROOT_PATH . 'public/';
-//        $fullPath = $basePath . $relativePath;
-//
-//        if (!is_dir($fullPath)) {
-//            return json(['code' => 1, 'msg' => '原图目录不存在']);
-//        }
-//
-//        // 1. 获取所有图片路径
-//        $allImages = glob($fullPath . '/*.{jpg,jpeg,png}', GLOB_BRACE);
-//        if (empty($allImages)) {
-//            return json(['code' => 0, 'msg' => '暂无图片', 'data' => [], 'total' => 0]);
-//        }
-//
-//        // 2. 构建路径信息映射(路径 => 文件信息)
-//        $imageInfoMap = [];
-//        foreach ($allImages as $imgPath) {
-//            $relative = str_replace('\\', '/', trim(str_replace($basePath, '', $imgPath), '/'));
-//            $info = @getimagesize($imgPath);
-//            $imageInfoMap[$relative] = [
-//                'width' => $info[0] ?? 0,
-//                'height' => $info[1] ?? 0,
-//                'size_kb' => round(filesize($imgPath) / 1024, 2),
-//                'created_time' => date('Y-m-d H:i:s', filectime($imgPath))
-//            ];
-//        }
-//
-//        $relativeImages = array_keys($imageInfoMap);
-//
-//        // 3. 获取数据库记录
-//        $dbRecords = Db::name('text_to_image')
-//            ->whereIn('old_image_url', $relativeImages)
-//            ->field('id as img_id, old_image_url, new_image_url, custom_image_url, chinese_description, english_description, img_name,status')
-//            ->select();
-//
-//        // 4. 构建映射表:路径 => 整条数据库记录
-//        $processedMap = [];
-//        foreach ($dbRecords as $item) {
-//            $key = str_replace('\\', '/', trim($item['old_image_url'], '/'));
-//            $processedMap[$key] = $item;
-//        }
-//
-//        // 5. 获取 same_count 统计
-//        $sameCountMap = Db::name('text_to_image')
-//            ->whereIn('old_image_url', $relativeImages)
-//            ->where('status', 1)
-//            ->group('old_image_url')
-//            ->column('count(*) as cnt', 'old_image_url');
-//
-//        // 6. 构建结果数据
-//        $allData = [];
-//        foreach ($relativeImages as $path) {
-//            $item = $processedMap[$path] ?? [];
-//            $info = $imageInfoMap[$path];
-//
-//            $isProcessed = !empty($item['img_name']) && !empty($item['custom_image_url']);
-//
-//            // 状态过滤
-//            if ($status === '1') {
-//                if (!$isProcessed) continue;
-//            } elseif ($status === '2') {
-//                if ($isProcessed) continue;
-//            }
-//
-//            $allData[] = [
-//                'path' => $path,
-//                'status' => (int)($item['status'] ?? 0),
-//                'same_count' => $sameCountMap[$path] ?? 0,//出图数量
-//                'is_processed' => $isProcessed ? 1 : 0,
-//                'new_image_url' => $item['new_image_url'] ?? '',
-//                'custom_image_url' => $item['custom_image_url'] ?? '',
-//                'chinese_description' => $item['chinese_description'] ?? '',
-//                'english_description' => $item['english_description'] ?? '',
-//                'img_name' => $item['img_name'] ?? '',
-//                'width' => $info['width'],
-//                'height' => $info['height'],
-//                'size_kb' => $info['size_kb'],
-//                'created_time' => $info['created_time']
-//            ];
-//        }
-//
-//        // 7. 分页处理
-//        $total = count($allData);
-//        $pagedData = array_slice(array_values($allData), ($page - 1) * $limit, $limit);
-//        foreach ($pagedData as $i => &$row) {
-//            $row['id'] = ($page - 1) * $limit + $i + 1;
-//        }
-//
-//        return json([
-//            'code' => 0,
-//            'msg' => '获取成功',
-//            'data' => $pagedData,
-//            'total' => $total,
-//            'page' => $page,
-//            'limit' => $limit
-//        ]);
-//    }
-
-
-
-//    public function getPreviewimg()
-//    {
-//        $page = (int)$this->request->param('page', 1);
-//        $limit = (int)$this->request->param('limit', 50);
-//        $status = $this->request->param('status', '');
-//
-//        $relativePath = $this->request->param('path', '');
-//        $basePath = ROOT_PATH . 'public/';
-//        $fullPath = $basePath . $relativePath;
-//
-//        if (!is_dir($fullPath)) {
-//            return json(['code' => 1, 'msg' => '目录不存在']);
-//        }
-//
-//        // 1. 获取所有图片路径(不再全部加载到内存)
-//        $allImages = glob($fullPath . '/*.{jpg,jpeg,png}', GLOB_BRACE);
-//        if (empty($allImages)) {
-//            return json(['code' => 0, 'msg' => '暂无图片', 'data' => [], 'total' => 0]);
-//        }
-//        // ✅ 加入排序:按照创建时间从新到旧
-//        usort($allImages, function ($a, $b) {
-//            return filectime($b) - filectime($a);
-//        });
-//        // 构建相对路径数组
-//        $relativeImages = array_map(function ($imgPath) use ($basePath) {
-//            return str_replace($basePath, '', $imgPath);
-//        }, $allImages);
-//
-//        // 2. 提前构建是否已出图map
-//        $dbRecords = Db::name('text_to_image')
-//            ->whereIn('old_image_url', $relativeImages)
-//            ->where('img_name', '<>', '')
-//            ->where('custom_image_url', '<>', '')
-//            ->where('status',1)
-//            ->field('id,old_image_url,new_image_url,custom_image_url,chinese_description,english_description,img_name')
-//            ->select();
-//
-//        $processedMap = [];
-//        foreach ($dbRecords as $item) {
-//            $processedMap[$item['old_image_url']] = $item;
-//        }
-//
-//        // 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. 构造最终筛选数据(分页前进行状态筛选)
-//        $filtered = [];
-//        foreach ($allImages as $imgPath) {
-//            $relative = str_replace($basePath, '', $imgPath);
-//            $processed = $processedMap[$relative] ?? null;
-//            $isProcessed = $processed ? 1 : 0;
-//
-//            // 状态过滤
-//            if ($status === 'processed' && !$isProcessed) continue;
-//            if ($status === 'unprocessed' && $isProcessed) continue;
-//
-//            $info = @getimagesize($imgPath); // 加@防止报错
-//            $sizeKB = round(filesize($imgPath) / 1024, 2);
-//            $ctime = date('Y-m-d H:i:s', filectime($imgPath));
-//
-//            $filtered[] = [
-//                'path' => $relative,
-//                'width' => $info[0] ?? 0,
-//                'height' => $info[1] ?? 0,
-//                'size_kb' => $sizeKB,
-//                'created_time' => $ctime,
-//                'img_name' => $processed['img_name'] ?? '',
-//                'is_processed' => $isProcessed,
-//                'new_image_url' => $processed['new_image_url'] ?? '',
-//                'custom_image_url' => $processed['custom_image_url'] ?? '',
-//                'chinese_description' => ($processed['chinese_description'] ?? '') . ($processed['english_description'] ?? ''),
-//                'english_description' => ($processed['english_description'] ?? '') . ($processed['english_description'] ?? ''),
-//                'same_count' => $sameCountMap[$relative] ?? 0
-//            ];
-//        }
-//
-//        // 5. 手动分页(对少量已筛选后的数据)
-//        $total = count($filtered);
-//        $pagedData = array_slice($filtered, ($page - 1) * $limit, $limit);
-//        foreach ($pagedData as $index => &$item) {
-//            $item['id'] = ($page - 1) * $limit + $index + 1;
-//        }
-//
-//        return json([
-//            'code' => 0,
-//            'msg' => '获取成功',
-//            'data' => $pagedData,
-//            'total' => $total,
-//            'page' => $page,
-//            'limit' => $limit
-//        ]);
-//    }
-
-
     /**
-     * 获取原图目录及每个目录下的图片数量
+     * 采用缓存机制
+     * 获取原图目录及每个目录下的图片数量(优化版)
      */
     public function getPreviewSubDirs()
     {
+        // 1. 设置基础路径
         $baseDir = rtrim(str_replace('\\', '/', ROOT_PATH), '/') . '/public/uploads/operate/ai/Preview';
         $baseRelativePath = 'uploads/operate/ai/Preview';
 
+        // 2. 检查目录是否存在
         if (!is_dir($baseDir)) {
             return json(['code' => 1, 'msg' => '目录不存在']);
         }
 
+        // 3. 获取目录最后修改时间作为缓存标识
+        $cacheKey = 'preview_dirs_' . md5($baseDir);
+        $lastModified = filemtime($baseDir);
+        $cacheVersionKey = $cacheKey . '_version';
+
+        // 4. 检查缓存版本是否匹配
+        if (cache($cacheVersionKey) != $lastModified) {
+            cache($cacheKey, null);
+            cache($cacheVersionKey, $lastModified, 86400);
+        }
+
+        // 5. 尝试从缓存获取
+        if (!$dirList = cache($cacheKey)) {
+            // 6. 重新扫描目录
+            $dirList = $this->scanDirectories($baseDir, $baseRelativePath);
+            cache($cacheKey, $dirList, 86400); // 缓存1天
+        }
+
+        return json([
+            'code' => 0,
+            'msg' => '获取成功',
+            'data' => $dirList
+        ]);
+    }
+
+    /**
+     * 扫描目录结构
+     */
+    private function scanDirectories($baseDir, $baseRelativePath)
+    {
         $dirs = [];
         $index = 1;
+        $processedDirs = [];
 
-        /**
-         * 递归扫描目录,提取含图片的子目录信息
-         */
-        $scanDir = function ($dirPath, $relativePath) use (&$scanDir, &$dirs, &$index) {
-            $items = scandir($dirPath);
+        $scanDir = function ($dirPath, $relativePath) use (&$scanDir, &$dirs, &$index, &$processedDirs) {
+            $items = @scandir($dirPath) ?: [];
             foreach ($items as $item) {
                 if ($item === '.' || $item === '..') continue;
 
@@ -427,67 +244,164 @@ class Facility extends Api
                 $relPath = $relativePath . '/' . $item;
 
                 if (is_dir($fullPath)) {
-                    // 递归子目录
-                    $scanDir($fullPath, $relPath);
-                } else {
-                    // 匹配图片文件
-                    if (preg_match('/\.(jpg|jpeg|png)$/i', $item)) {
-                        $parentDir = dirname($fullPath);
-                        $relativeDir = dirname($relPath);
-                        $key = md5($parentDir);
-
-                        if (!isset($dirs[$key])) {
-                            $ctime = filectime($parentDir);
-
-                            // 数据库统计:已处理图片数量
-                            $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();
-
-                            // 当前目录下图片数量
-                            $imageFiles = glob($parentDir . '/*.{jpg,jpeg,png}', GLOB_BRACE);
-                            $imageCount = is_array($imageFiles) ? count($imageFiles) : 0;
-
-                            $dirs[$key] = [
-                                'id' => $index++,
-                                'name' => basename($parentDir),
-                                'count' => $hasData,
-                                'ctime' => $ctime, // 时间戳,用于排序
-                                'ctime_text' => date('Y-m-d H:i:s', $ctime), // 格式化日期,用于显示
-                                'image_count' => $imageCount,
-                                'new_image_url' => "/uploads/operate/ai/dall-e/",
-                                'old_image_url' => $relativeDir
-                            ];
-                        }
+                    $dirKey = md5($fullPath);
+                    if (!isset($processedDirs[$dirKey])) {
+                        $processedDirs[$dirKey] = true;
+                        $scanDir($fullPath, $relPath);
+                    }
+                } elseif (preg_match('/\.(jpg|jpeg|png)$/i', $item)) {
+                    $parentDir = dirname($fullPath);
+                    $relativeDir = dirname($relPath);
+                    $key = md5($parentDir);
+
+                    if (!isset($dirs[$key])) {
+                        $ctime = @filectime($parentDir) ?: time();
+
+                        // 数据库查询
+                        $hasData = Db::name('text_to_image')
+                            ->where('custom_image_url', '<>', '')
+                            ->where('img_name', '<>', '')
+                            ->whereLike('old_image_url', $relativeDir . '/%')
+                            ->where('status',1)
+                            ->cache(true, 300)
+                            ->count();
+
+                        $imageFiles = @glob($parentDir . '/*.{jpg,jpeg,png}', GLOB_BRACE);
+                        $imageCount = $imageFiles ? count($imageFiles) : 0;
+
+                        $dirs[$key] = [
+                            'id' => $index++,
+                            'name' => basename($parentDir),
+                            'count' => $hasData,
+                            'ctime' => $ctime,
+                            'ctime_text' => date('Y-m-d H:i:s', $ctime),
+                            'image_count' => $imageCount,
+                            'new_image_url' => "/uploads/operate/ai/dall-e/",
+                            'old_image_url' => $relativeDir
+                        ];
                     }
                 }
             }
         };
 
-        // 执行目录扫描
         $scanDir($baseDir, $baseRelativePath);
 
-        // 排序:按照创建时间(从新到旧)
+        // 按ID降序排序
         $dirList = array_values($dirs);
         usort($dirList, function ($a, $b) {
-            return $b['ctime'] - $a['ctime'];
+            return $b['id'] - $a['id'];
         });
 
-        return json([
-            'code' => 0,
-            'msg' => '获取成功',
-            'data' => $dirList
-        ]);
+        return $dirList;
+    }
+
+    /**
+     * 手动清除缓存(在目录变更后调用)
+     */
+    public function clearCache()
+    {
+        $baseDir = rtrim(str_replace('\\', '/', ROOT_PATH), '/') . '/public/uploads/operate/ai/Preview';
+        $cacheKey = 'preview_dirs_' . md5($baseDir);
+
+        cache($cacheKey, null);
+        cache($cacheKey . '_version', null);
+
+        return json(['code' => 0, 'msg' => '缓存已清除']);
     }
 
+    /**
+     * 不使用缓存机制(查询速度较慢)
+     * 获取原图目录及每个目录下的图片数量
+     */
+//    public function getPreviewSubDirs()
+//    {
+//        $baseDir = rtrim(str_replace('\\', '/', ROOT_PATH), '/') . '/public/uploads/operate/ai/Preview';
+//        $baseRelativePath = 'uploads/operate/ai/Preview';
+//
+//        if (!is_dir($baseDir)) {
+//            return json(['code' => 1, 'msg' => '目录不存在']);
+//        }
+//
+//        $dirs = [];
+//        $index = 1;
+//
+//        /**
+//         * 递归扫描目录,提取含图片的子目录信息
+//         */
+//        $scanDir = function ($dirPath, $relativePath) use (&$scanDir, &$dirs, &$index) {
+//            $items = scandir($dirPath);
+//            foreach ($items as $item) {
+//                if ($item === '.' || $item === '..') continue;
+//
+//                $fullPath = $dirPath . '/' . $item;
+//                $relPath = $relativePath . '/' . $item;
+//
+//                if (is_dir($fullPath)) {
+//                    // 递归子目录
+//                    $scanDir($fullPath, $relPath);
+//                } else {
+//                    // 匹配图片文件
+//                    if (preg_match('/\.(jpg|jpeg|png)$/i', $item)) {
+//                        $parentDir = dirname($fullPath);
+//                        $relativeDir = dirname($relPath);
+//                        $key = md5($parentDir);
+//
+//                        if (!isset($dirs[$key])) {
+//                            $ctime = filectime($parentDir);
+//
+//                            // 数据库统计:已处理图片数量
+//                            $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();
+//
+//                            // 当前目录下图片数量
+//                            $imageFiles = glob($parentDir . '/*.{jpg,jpeg,png}', GLOB_BRACE);
+//                            $imageCount = is_array($imageFiles) ? count($imageFiles) : 0;
+//
+//                            $dirs[$key] = [
+//                                'id' => $index++,
+//                                'name' => basename($parentDir),
+//                                'count' => $hasData,
+//                                'ctime' => $ctime, // 时间戳,用于排序
+//                                'ctime_text' => date('Y-m-d H:i:s', $ctime), // 格式化日期,用于显示
+//                                'image_count' => $imageCount,
+//                                'new_image_url' => "/uploads/operate/ai/dall-e/",
+//                                'old_image_url' => $relativeDir
+//                            ];
+//                        }
+//                    }
+//                }
+//            }
+//        };
+//
+//        // 执行目录扫描
+//        $scanDir($baseDir, $baseRelativePath);
+//
+//        // 排序:按照创建时间(从新到旧)
+//        $dirList = array_values($dirs);
+////        usort($dirList, function ($a, $b) {
+////            return $b['ctime'] - $a['ctime'];
+////        });
+//
+//        usort($dirList, function ($a, $b) {
+//            return $b['id'] - $a['id'];
+//        });
+//
+//
+//        return json([
+//            'code' => 0,
+//            'msg' => '获取成功',
+//            'data' => $dirList
+//        ]);
+//    }
+
 
     /**
      * 图片上传
-     * @return void
      */
     public function ImgUpload()
     {
@@ -500,10 +414,10 @@ class Facility extends Api
             exit(204);
         }
 
-// 实际请求必须返回 CORS 头
+        // 实际请求必须返回 CORS 头
         header('Access-Control-Allow-Origin: *');
 
-// 获取上传的文件
+        // 获取上传的文件
         $file = request()->file('image');
 
         if ($file) {
@@ -536,16 +450,17 @@ class Facility extends Api
 
 
     /**
-     * 模版
+     * 查询模版
      */
     public function Template(){
-        $Template = Db::name("template")->find();
+        $Template = Db::name("template")->where('ids',1)->find();
         return json([
             'code' => 0,
             'msg' => '模版',
             'data' => $Template
         ]);
     }
+
     /**
      * 更新模版
      */
@@ -553,26 +468,18 @@ class Facility extends Api
         if (Request::instance()->isPost() == false){
             $this->error('非法请求');
         }
-
         $params = Request::instance()->post();
-
-
-        // 验证传入的参数是否存在,避免空值
         if (empty($params['textareaContent']) || empty($params['width']) || empty($params['height'])) {
             return json(['code' => 1, 'msg' => '参数缺失']);
         }
-
-        // 更新模板数据
         $Template = Db::name("template")
-            ->where('id', 1) // 假设模板 ID 是 1,需根据实际情况修改
+            ->where('ids', 1)
             ->update([
                 'english_content' => $params['english_content'],  // 更新文生文模版内容
                 'content' => $params['textareaContent'],  // 更新图生文模版内容
                 'width' => $params['width'],  // 更新宽度
                 'height' => $params['height'],  // 更新宽度
             ]);
-
-        // 判断数据库更新是否成功
         if ($Template){
             return json(['code' => 0, 'msg' => '成功']);
         }else{
@@ -580,4 +487,69 @@ class Facility extends Api
         }
     }
 
+    /**
+     * 打包图片
+     */
+    public function packImagess()
+    {
+        try {
+            $params = $this->request->post();
+            $paths = $params['paths'] ?? [];
+
+            if (empty($paths) || !is_array($paths)) {
+                return json(['code' => 1, 'msg' => '路径参数不能为空或格式不正确']);
+            }
+
+            // 设置基础路径和压缩目录路径
+            $basePath = ROOT_PATH . 'public/';
+            $zipDir = $basePath . 'uploads/operate/ai/zip/';
+
+            if (!is_dir($zipDir)) {
+                mkdir($zipDir, 0755, true);
+            }
+
+            // 压缩包文件名及完整路径
+            $fileName = 'images_' . date('Ymd_His') . '.zip';
+            $zipPath = $zipDir . $fileName;
+
+            // 创建 Zip 文件
+            $zip = new \ZipArchive();
+            if ($zip->open($zipPath, \ZipArchive::CREATE) !== TRUE) {
+                return json(['code' => 1, 'msg' => '无法创建压缩包']);
+            }
+
+            // 添加文件到压缩包
+            $addCount = 0;
+            foreach ($paths as $relativePath) {
+                $relativePath = ltrim($relativePath, '/');
+                $fullPath = $basePath . $relativePath;
+
+                if (file_exists($fullPath)) {
+                    $zip->addFile($fullPath, basename($fullPath)); // 仅保存文件名
+                    $addCount++;
+                }
+            }
+
+            $zip->close();
+
+            if ($addCount === 0) {
+                return json(['code' => 1, 'msg' => '未找到有效图片,未生成压缩包']);
+            }
+
+            // 返回下载地址(注意路径与保存路径一致)
+            $downloadUrl = request()->domain() . '/uploads/operate/ai/zip/' . $fileName;
+
+            return json([
+                'code' => 0,
+                'msg' => '打包成功',
+                'download_url' => $downloadUrl
+            ]);
+        } catch (\Exception $e) {
+            return json([
+                'code' => 1,
+                'msg' => '异常错误:' . $e->getMessage()
+            ]);
+        }
+    }
+
 }

+ 42 - 547
application/api/controller/WorkOrder.php

@@ -1,5 +1,4 @@
 <?php
-
 namespace app\api\controller;
 
 use app\common\controller\Api;
@@ -11,12 +10,10 @@ use think\Log;
 use think\Queue;
 use think\queue\job\Redis;
 
-
 class WorkOrder extends Api
 {
     protected $noNeedLogin = ['*'];
     protected $noNeedRight = ['*'];
-
     /**
      * 出图接口
      * 此方法处理图像转换为文本的请求,将图像信息存入队列以供后续处理。
@@ -31,6 +28,7 @@ class WorkOrder extends Api
 
     /**
      * 查询队列列表
+     * 统计文件对应的队列情况
      */
     public function get_queue_logs()
     {
@@ -67,25 +65,31 @@ class WorkOrder extends Api
                         break;
                 }
             }
+            // ✅ 只保留排队中的数量大于 0 的记录
+            if ($log['排队中的数量'] > 0) {
+                $result[] = $log;
+            }
         }
         return json([
             'code' => 0,
             'msg' => '查询成功',
-            'data' => $queue_logs,
-            'count' => count($queue_logs)
+            'data' => $result,
+            'count' => count($result)
         ]);
+
     }
 
     /**
-     * 查询总队列状态
+     * 查询总队列状态(统计当前处理的数据量)
      */
     public function queueStats()
     {
         $statusList = Db::name('image_task_log')
             ->field('status, COUNT(*) as total')
-            ->whereTime('create_time', 'today')
+            ->where('create_time', '>=', date('Y-m-d 00:00:00'))
             ->group('status')
             ->select();
+
         $statusCount = [];
         foreach ($statusList as $item) {
             $statusCount[$item['status']] = $item['total'];
@@ -109,7 +113,6 @@ class WorkOrder extends Api
         ]);
     }
 
-
     /**
      * 显示当前运行中的队列监听进程
      */
@@ -117,13 +120,31 @@ class WorkOrder extends Api
     {
         $redis = new \Redis();
         $redis->connect('127.0.0.1', 6379);
+        $redis->auth('123456');
         $redis->select(15);
+
         $key = 'queues:imgtotxt';
+
+        // 判断 key 是否存在,避免报错
+        if (!$redis->exists($key)) {
+            return json([
+                'code' => 0,
+                'msg'  => '查询成功,队列为空',
+                'count' => 0,
+                'tasks_preview' => []
+            ]);
+        }
+
         $count = $redis->lLen($key);
         $list = $redis->lRange($key, 0, 9);
-        $parsed = array_map(function ($item) {
+
+        // 解码 JSON 内容,确保每一项都有效
+        $parsed = array_filter(array_map(function ($item) {
             return json_decode($item, true);
-        }, $list);
+        }, $list), function ($item) {
+            return !is_null($item);
+        });
+
         return json([
             'code' => 0,
             'msg'  => '查询成功',
@@ -133,21 +154,21 @@ class WorkOrder extends Api
     }
 
     /**
-     * 清空队列(同时删除近30分钟的队列日志)
+     * 清空队列并删除队列日志记录
      */
     public function stopQueueProcesses()
     {
         $redis = new \Redis();
         $redis->connect('127.0.0.1', 6379);
+        $redis->auth('123456');
         $redis->select(15);
 
         $key_txttoimg = 'queues:txttoimg';
         $key_txttotxt = 'queues:txttotxt';
         $key_imgtotxt = 'queues:imgtotxt';
-        $count = $redis->lLen($key_txttoimg);
-        $count1 = $redis->lLen($key_txttotxt);
-        $count2 = $redis->lLen($key_imgtotxt);
-        $count = $count+$count1+$count2;
+
+        $count = $redis->lLen($key_txttoimg) + $redis->lLen($key_txttotxt) + $redis->lLen($key_imgtotxt);
+
         if ($count === 0) {
             return json([
                 'code' => 1,
@@ -160,13 +181,17 @@ class WorkOrder extends Api
         $redis->del($key_txttotxt);
         $redis->del($key_imgtotxt);
 
+        // 删除数据库中 log = '队列中' 的记录
+        Db::name('image_task_log')
+            ->where('log', '队列中')
+            ->delete();
+
         return json([
             'code' => 0,
-            'msg'  => '已成功停止队列任务'
+            'msg'  => '已成功停止队列任务并清除队列日志'
         ]);
     }
 
-
     /**
      * 开启队列任务
      * 暂时用不到、服务器已开启自动开启队列模式
@@ -198,535 +223,5 @@ class WorkOrder extends Api
 //        }
 //    }
 
-    /**
-     * 查看总队列任务
-     */
-//    public function queueStats()
-//    {
-//        $statusCounts = Db::name('image_task_log')
-//            ->field('status, COUNT(*) as total')
-//            ->whereTime('create_time', 'today')
-//            ->order('id desc')
-//            ->select();
-//
-//        $result = [
-//            '待处理' => 0,
-//            '处理中' => 0,
-//            '成功' => 0,
-//            '失败' => 0
-//        ];
-//        $total = 0;
-//
-//        foreach ($statusCounts as $row) {
-//            $count = $row['total'];
-//            $total += $count;
-//            switch ($row['status']) {
-//                case 0:
-//                    $result['待处理'] = $count;
-//                    break;
-//                case 1:
-//                    $result['处理中'] = $count;
-//                    break;
-//                case 2:
-//                    $result['成功'] = $count;
-//                    break;
-//                case 3:
-//                    $result['失败'] = $count;
-//                    break;
-//            }
-//        }
-//
-//        return json([
-//            'code' => 0,
-//            'msg' => '获取成功',
-//            'data' => ['总任务数' => $total] + $result
-//        ]);
-//    }
-
-
-
-
-
-
-    //单个调用[可以用来做测试]
-//    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();
-//
-//        // 统一路径格式
-//        $sourceDirRaw = str_replace('\\', '/', trim($params['sourceDir'] ?? '', '/'));
-//        $fileName = trim($params['file_name'] ?? '');
-//
-//        // 自动拆分文件名
-//        if (!$fileName && preg_match('/([^\/]+\.(jpg|jpeg|png))$/i', $sourceDirRaw, $matches)) {
-//            $fileName = $matches[1];
-//            $sourceDirRaw = preg_replace('/\/' . preg_quote($fileName, '/') . '$/', '', $sourceDirRaw);
-//        }
-//
-//        // 参数校验
-//        if ($sourceDirRaw === '' || $fileName === '') {
-//            return $this->error('参数错误:sourceDir 或 file_name 不能为空');
-//        }
-//
-//        // 构建路径
-//        $rootPath = str_replace('\\', '/', ROOT_PATH);
-//        $sourceDir = rtrim($rootPath . 'public/' . $sourceDirRaw, '/') . '/';
-//        $filePath = $sourceDir . $fileName;
-//        $relativePath = $sourceDirRaw . '/' . $fileName;
-//
-//        // 文件检查
-//        if (!is_dir($sourceDir)) {
-//            return $this->error('源目录不存在:' . $sourceDir);
-//        }
-//        if (!is_file($filePath)) {
-//            return $this->error('文件不存在:' . $filePath);
-//        }
-//
-//        // 避免重复处理
-////        $exists = Db::name('text_to_image')
-////            ->where('old_image_url', $relativePath)
-////            ->where('chinese_description', '<>', '')
-////            ->find();
-////        if ($exists) {
-////            return $this->success('该图片已生成描述,无需重复处理');
-////        }
-//
-////        try {
-//            // 获取图片信息
-//            $ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
-//            $mime = ($ext === 'jpg' || $ext === 'jpeg') ? 'jpeg' : $ext;
-//
-//            list($width, $height) = getimagesize($filePath);
-//            $imageData = base64_encode(file_get_contents($filePath));
-//            if (!$imageData || strlen($imageData) < 1000) {
-//                throw new \Exception('图片内容读取失败');
-//            }
-//            $imageUrl = "data:image/{$mime};base64,{$imageData}";
-//
-//            // 构建严格格式的提示词
-//            $userPrompt = preg_replace('/\s+/u', '', $params['prompt']); // 移除所有空白字符
-//            $strictPrompt = "严格遵守以下规则:
-//1. 只返回三段内容:
-//   第一段:纯中文图案描述
-//   第二段:---json json---
-//   第三段:纯英文图案描述
-//2. 描述中必须体现图案的类型、颜色、风格等关键信息
-//3. 不允许添加任何解释、引导、说明、示例等文字,必须只包含图案描述内容本身
-//3. 示例:
-//这张图中的图案是代表达拉斯足球队的标志,包括一个头盔图形和围绕它的文字。头盔以灰色和白色为主,有蓝色和黑色的细节。
-//---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;
-//
-//            // 调用 GPT 接口
-//            $gptRes = $this->callGptApi($imageUrl, $strictPrompt);
-//            $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
-//
-//            // 验证 GPT 返回格式
-//            if (strpos($gptText, '---json json---') === false) {
-//                throw new \Exception('GPT 返回格式不正确,缺少分隔符');
-//            }
-//
-//            list($chineseDesc, $englishDesc) = array_map('trim', explode('---json json---', $gptText));
-//
-//            if ($chineseDesc === '' || $englishDesc === '') {
-//                throw new \Exception('描述内容为空,请检查 GPT 返回');
-//            }
-//
-//            // 插入数据库(成功时才插入)
-//            $this->logToDatabase([
-//                'old_image_url'       => $relativePath,
-//                'chinese_description' => $chineseDesc,
-//                'english_description' => $englishDesc,
-//                'size'                => "",
-//                'status'              => 1
-//            ]);
-//
-//            return $this->success('图生文成功', [
-//                'chinese' => $chineseDesc,
-//                'english' => $englishDesc
-//            ]);
-//
-////        } catch (\Exception $e) {
-////            // 只写日志,不重复插入数据库
-//////            Log::error('图生文失败 [' . $relativePath . ']:' . $e->getMessage());
-////            Db::name('text_to_image')->insert([
-////                'old_image_url' => $relativePath,
-////                'error_msg'     => $e->getMessage(),
-////                'status'        => 0,
-////                'create_time'   => date('Y-m-d H:i:s') // 可选:记录时间戳
-////            ]);
-////            return json([
-////                'code' => 1,
-////                'msg'  => '图生文失败:' . $e->getMessage()
-////            ]);
-////        }
-//    }
-//
-//    /**
-//     * 调用GPT API生成文字描述(改进版)
-//     */
-//    public function callGptApi($imageUrl, $prompt)
-//    {
-//        $data = [
-//            "model" => "gpt-4-vision-preview",
-//            "messages" => [[
-//                "role" => "user",
-//                "content" => [
-//                    ["type" => "text", "text" => $prompt],
-//                    ["type" => "image_url", "image_url" => [
-//                        "url" => $imageUrl,
-//                        "detail" => "auto" // ✅ 显式添加 detail 字段,兼容 vision API
-//                    ]]
-//                ]
-//            ]],
-//            "max_tokens" => 1000
-//        ];
-//
-//        return $this->callApi($this->config['gpt']['api_url'], $this->config['gpt']['api_key'], $data);
-//    }
-//
-//
-//    /**
-//     * 文字生成图片(第二步) - 等比缩放生成指定尺寸图
-//     */
-//    public function textToImage()
-//    {
-//        $params = $this->request->param();
-//        $fileName = trim($params['file_name'] ?? '');
-//        $outputDirRaw = trim($params['outputDir'] ?? '', '/');
-//        $width = intval($params['width'] ?? 512);
-//        $height = intval($params['height'] ?? 512);
-//        $prompt = trim($params['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);
-////        if (empty($prompt) || mb_strlen($prompt) < 10) {
-////            return json(['code' => 1, 'msg' => '提示词过短或为空,请填写有效的英文图像描述']);
-////        }
-////        if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $prompt)) {
-////            return json(['code' => 1, 'msg' => '请使用英文提示词,当前提示内容包含中文']);
-////        }
-//
-//        // 查询图像记录
-//        $record = Db::name('text_to_image')
-//            ->where('old_image_url', 'like', "%{$fileName}")
-//            ->order('id desc')
-//            ->find();
-////        echo "<pre>";
-////        print_r($record);
-////        echo "<pre>";
-//
-//        if (!$record) {
-//            return json(['code' => 1, 'msg' => '没有找到匹配的图像记录']);
-//        }
-//
-////        try {
-//            // 日志记录
-//            $logDir = ROOT_PATH . '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 API
-//            $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) {
-//                throw new \Exception("下载图像失败或内容异常");
-//            }
-//
-//            // 保存原图
-//            $filename1024 = 'dalle_' . md5($record['old_image_url'] . microtime()) . '_1024.png';
-//            $savePath1024 = $fullBaseDir . '1024x1024/' . $filename1024;
-//            file_put_contents($savePath1024, $imgData1024);
-//
-//            // 创建图像资源
-//            $im = @imagecreatefromstring($imgData1024);
-//            if (!$im) {
-//                throw new \Exception("图像格式不受支持或已损坏");
-//            }
-//
-//// 获取原图尺寸
-//            $srcWidth = imagesx($im);
-//            $srcHeight = imagesy($im);
-//
-//// 目标尺寸
-//            $targetWidth = $width;
-//            $targetHeight = $height;
-//
-//// 计算缩放比例(以覆盖为目标,可能会超出一边)
-//            $ratio = max($targetWidth / $srcWidth, $targetHeight / $srcHeight);
-//            $newWidth = intval($srcWidth * $ratio);
-//            $newHeight = intval($srcHeight * $ratio);
-//
-//// 创建目标图像(目标尺寸)
-//            $dstImg = imagecreatetruecolor($targetWidth, $targetHeight);
-//
-//// 缩放后居中裁剪偏移
-//            $offsetX = intval(($newWidth - $targetWidth) / 2);
-//            $offsetY = intval(($newHeight - $targetHeight) / 2);
-//
-//// 临时缩放图像
-//            $tempImg = imagecreatetruecolor($newWidth, $newHeight);
-//            imagecopyresampled($tempImg, $im, 0, 0, 0, 0, $newWidth, $newHeight, $srcWidth, $srcHeight);
-//
-//// 裁剪中间部分到最终尺寸
-//            imagecopy($dstImg, $tempImg, 0, 0, $offsetX, $offsetY, $targetWidth, $targetHeight);
-//
-//// 保存结果
-//            $filenameCustom = 'dalle_' . md5($record['old_image_url'] . microtime()) . "_custom.png";
-//            $savePathCustom = $fullBaseDir . "{$width}x{$height}/" . $filenameCustom;
-//            imagepng($dstImg, $savePathCustom);
-//
-//// 释放内存
-//            imagedestroy($im);
-//            imagedestroy($tempImg);
-//            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')
-//            ]);
-//
-//            return json([
-//                'code' => 0,
-//                'msg'  => '文生图生成完成',
-//                'data' => [
-//                    'new_image_url'    => str_replace($rootPath . 'public/', '', $savePath1024)?? '',
-//                    'custom_image_url' => str_replace($rootPath . 'public/', '', $savePathCustom)?? '',
-//                    'size'             => "{$width}x{$height}"
-//                ]
-//            ]);
-//
-////        } catch (\Exception $e) {
-////            Db::name('text_to_image')->where('id', $record['id'])->update([
-////                'status'    => 0,
-////                'error_msg' => $e->getMessage()
-////            ]);
-////
-////            return json(['code' => 1, 'msg' => '文生图失败:' . $e->getMessage()]);
-////        }
-//    }
-//
-//    /**
-//     * 调用 DALL·E 接口
-//     * 文生图
-//     */
-//    public function callDalleApi($prompt)
-//    {
-//        $data = [
-//            'prompt' => $prompt,
-//            'model'  => 'dall-e-2',
-//            'n'      => 1,
-//            'size'   => '1024x1024'
-//        ];
-//        return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
-////        $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}");
-//    }
-//
-//    /**
-//     * 记录到数据库
-//     */
-//    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'] : '',
-//            'chinese_description' => $data['chinese_description'] ?? '',
-//            'english_description' => $data['english_description'] ?? '',
-//            'model' => 'dall-e-2',
-//            'quality' => 'standard',
-//            'style' => 'vivid',
-//            'status' => $data['status'] ?? 0,
-//            'error_msg' => $data['error_msg'] ?? '',
-//            'created_time' => date('Y-m-d H:i:s'),
-//            'updated_time' => date('Y-m-d H:i:s')
-//        ];
-//
-//        if (isset($data['id'])) {
-//            Db::name('text_to_image')->where('id', $data['id'])->update($record);
-//        } else {
-//            Db::name('text_to_image')->insert($record);
-//        }
-//    }
-//
-//    /**
-//     * 获取待处理的源图片列表
-//     */
-//    public function getSourceImages()
-//    {
-//        $params = $this->request->param();
-//        $sourceDir = rtrim(ROOT_PATH . 'public/' . $params['sourceDir'], '/') . '/';
-//
-//        if (!is_dir($sourceDir)) return $this->error('源目录不存在');
-//
-//        $files = glob($sourceDir . '*.{jpg,jpeg,png}', GLOB_BRACE);
-//        $result = [];
-//
-//        foreach ($files as $file) {
-//            $relativePath = trim($params['sourceDir'], '/') . '/' . basename($file);
-//
-//            $exists = Db::name('text_to_image')
-//                ->where('old_image_url', $relativePath)
-//                ->where('status', 1)
-//                ->find();
-//
-//            if (!$exists) {
-//                $result[] = basename($file);
-//            }
-//        }
-//
-//        return $this->success('获取成功', $result);
-//    }
-//
-//    /**
-//     * 获取处理成功的列表
-//     */
-//    public function getlist()
-//    {
-//        $today = date('Y-m-d');
-//        $tomorrow = date('Y-m-d', strtotime('+1 day'));
-//
-//        $res = Db::name('text_to_image')
-//            ->where('status', 1)
-//            ->where('created_time', '>=', $today . ' 00:00:00')
-//            ->where('created_time', '<', $tomorrow . ' 00:00:00')
-//            ->order('id desc')
-//            ->select();
-//
-//        foreach ($res as &$item) {
-//            $item['status_text'] = '成功';
-//        }
-//
-//        return json(['code' => 0, 'msg' => '获取成功', 'data' => $res]);
-//    }
-//
-//    /**
-//     * 获取错误日志
-//     */
-//    public function getErrorLogs()
-//    {
-//        $res = Db::name('text_to_image')
-//            ->where('status', 0)
-//            ->order('id desc')
-//            ->limit(20)
-//            ->select();
-//
-//        return json(['code' => 0, 'msg' => '获取失败记录成功', 'data' => $res]);
-//    }
-
-
 
 }

+ 1 - 1
application/extra/queue.php

@@ -5,7 +5,7 @@ return [
     'default'    => 'default',    // 默认的队列名称
     'host'       => '127.0.0.1',       // redis 主机ip
     'port'       => 6379,        // redis 端口
-    'password'   => '',             // redis 密码
+    'password'   => '123456',             // redis 密码
     'select'     => 15,          // 使用哪一个 db,默认为 db0
     'timeout'    => 0,          // redis连接的超时时间
     'persistent' => false,

+ 11 - 8
application/job/ImageArrJob.php

@@ -9,12 +9,14 @@ class ImageArrJob
 {
     public function fire(Job $job, $data)
     {
+
+        //队列id
         $task_id = $data['task_id'];
+        //队列数据
         $images = $data['data'];
 
         foreach ($images as $value) {
             $value['task_id'] = $task_id;
-
             // 1. 清理 sourceDir,去掉末尾重复的 Preview
             $sourceDir = rtrim($value["sourceDir"], '/\\');
             $dirParts = explode('/', str_replace('\\', '/', $sourceDir));
@@ -33,29 +35,30 @@ class ImageArrJob
             $log_id = Db::name('image_task_log')->insertGetId([
                 'task_id' => $task_id,
                 'file_name' => $fullPath,
-                'type' => $value['type'] ?? '',
+                'model_name' => $value['type'] ?? '',
                 'status' => 0,
                 'log' => '队列中',
                 'create_time' => date('Y-m-d H:i:s')
             ]);
-            $value['log_id'] = $log_id;
 
-            // 5. 推送任务到对应队列
+            $value['log_id'] = $log_id;
+            // 6. 若有链式任务,传递下去
+            $chain_next = $value['chain_next'] ?? [];
             switch (trim($value['type'])) {
                 case '图生文':
-                    Queue::push('app\job\ImageJob', $value, 'imgtotxt');
+                    Queue::push('app\job\ImageJob', array_merge($value, ['chain_next' => $chain_next]), 'imgtotxt');
                     break;
                 case '文生文':
-                    Queue::push('app\job\TextToTextJob', $value, 'txttotxt');
+                    Queue::push('app\job\TextToTextJob', array_merge($value, ['chain_next' => $chain_next]), 'txttotxt');
                     break;
                 case '文生图':
-                    Queue::push('app\job\TextToImageJob', $value, 'txttoimg');
+                    Queue::push('app\job\TextToImageJob', array_merge($value, ['chain_next' => $chain_next]), 'txttoimg');
                     break;
                 default:
-                    // 类型未知时不推送,并可写入错误日志
                     \think\Log::warning("未识别的任务类型:" . json_encode($value, JSON_UNESCAPED_UNICODE));
                     break;
             }
+
         }
 
         // 6. 更新任务状态为已启动

+ 45 - 177
application/job/ImageJob.php

@@ -1,28 +1,15 @@
 <?php
 namespace app\job;
+use app\service\AIGatewayService;
 use think\Db;
 use think\queue\Job;
 use think\Queue;
+/**
+ * 图生文队列任务
+ */
 class ImageJob{
-    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 "<pre>";
-        // print_r($data);
-        // echo "<pre>";die;
 
         $logId = $data['log_id'] ?? null;
 
@@ -49,6 +36,19 @@ class ImageJob{
             }
 
             echo date('Y-m-d H:i:s')."图生文结束\n";
+
+            //链式任务:图生文成功后继续推送文生文
+            if (!empty($data['chain_next'])) {
+                $nextType = array_shift($data['chain_next']); // 获取下一个任务类型
+                $data['type'] = $nextType;
+
+                // 保留剩余链,继续传下去
+                Queue::push('app\job\ImageArrJob', [
+                    'task_id' => $data['task_id'],
+                    'data' => [ $data ]
+                ], 'arrimage');
+            }
+
             $job->delete();
         } catch (\Exception $e) {
             //异常处理,记录失败日志
@@ -92,6 +92,7 @@ class ImageJob{
      */
     public function imageToText($sourceDirRaw, $fileName, $prompt, $call_data)
     {
+
         // 自动拆分文件名
         if (!$fileName && preg_match('/([^\/]+\.(jpg|jpeg|png))$/i', $sourceDirRaw, $matches)) {
             $fileName = $matches[1];
@@ -109,10 +110,13 @@ class ImageJob{
         $filePath = $sourceDir . $fileName;
         $relativePath = $sourceDirRaw . '/' . $fileName;
 
-        // 文件检查
+
+        // 文件夹是否存在(绝对路径检查)
         if (!is_dir($sourceDir)) {
             return '源目录不存在:' . $sourceDir;
         }
+
+        // 文件是否存在
         if (!is_file($filePath)) {
             return '文件不存在:' . $filePath;
         }
@@ -133,25 +137,19 @@ class ImageJob{
         if (!is_dir($logDir)) mkdir($logDir, 0755, true);
 
         // 调用图生文
-        $gptRes = $this->callGptApi($imageUrl, $prompt);
-        $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
-
+        $ai = new AIGatewayService();
+        $gptRes = $ai->callGptApi($imageUrl, $prompt);
 
-        // 提取英文描述
-        $patternEnglish = '/^([\s\S]+?)---json json---/';
-        preg_match($patternEnglish, $gptText, $matchEn);
-        $englishDesc = isset($matchEn[1]) ? trim($matchEn[1]) : '';
-
-        // 提取中文描述
-        $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]) : '';
+        $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
 
-        // 提取图片名(可能是中文短句,也可能是关键词)
-        $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);
+        //gtp返回内容日志
+        // file_put_contents(
+        //     $logDir . 'img_name_success.txt',
+        //     "\n======== " . date('Y-m-d H:i:s') . " ========\n" .
+        //   $gptText. "\n\n",
+        //     FILE_APPEND
+        // );
+        // echo "<pre>";print_r($gptText);echo "<pre>"; return 0;die;
 
         // 验证 GPT 返回格式
         if (strpos($gptText, '---json json---') === false) {
@@ -184,152 +182,22 @@ class ImageJob{
         // );
 
         // 成功写入数据库
-        $this->logToDatabase([
-            'img_name' => $img_name,
-            'old_image_url' => $relativePath,
+        $record = [
             'chinese_description' => $chineseDesc,
             'english_description' => $englishDesc,
+            'old_image_url' => $relativePath,
+            'new_image_url' => '',
+            'custom_image_url' => '',
+            'img_name' => $img_name,
+            'status' => 0,
+            'status_name' => "图生文",
+            'model' => "",
             'size' => "",
-            'status' => 0
-        ]);
-        return ;
-    }
-
-
-    /**
-     * 文生文接口
-     */
-    public function textToTxt($prompt,$id)
-    {
-
-        // 查询数据库记录
-        $record = Db::name('text_to_image')
-            ->field('id,english_description')
-            ->where('id',$id)
-            ->order('id desc')
-            ->find();
-
-        if (!$record) {
-            return '没有找到匹配的图像记录';
-        }
-
-        // 调用文生文
-        $gptRes = $this->TxtGptApi($prompt.$record['english_description']);
-        $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
-
-
-        // 更新数据库记录
-        Db::name('text_to_image')->where('id', $record['id'])->update([
-            'english_description' => $gptText
-        ]);
-        return 0;
-    }
-
-    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'] ?? '',
-            'img_name' => $data['img_name'],
-            'model' => '',
-            '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'] ?? '',
-            'status' => $data['status'] ?? 0,
-            'error_msg' => $data['error_msg'] ?? '',
+            'error_msg' => '',
             'created_time' => date('Y-m-d H:i:s'),
-            'updated_time' => date('Y-m-d H:i:s')
-        ];
-
-        if (isset($data['id'])) {
-            Db::name('text_to_image')->where('id', $data['id'])->update($record);
-        } else {
-            Db::name('text_to_image')->insert($record);
-        }
-    }
-
-    /**
-     * 图升文模型
-     */
-    public function callGptApi($imageUrl, $prompt)
-    {
-        $data = [
-            "model" => "gpt-4-vision-preview",
-            "messages" => [[
-                "role" => "user",
-                "content" => [
-                    ["type" => "text", "text" => $prompt],
-                    ["type" => "image_url", "image_url" => [
-                        "url" => $imageUrl,
-                        "detail" => "auto" // ✅ 显式添加 detail 字段,兼容 vision API
-                    ]]
-                ]
-            ]],
-            "max_tokens" => 1000
-        ];
-
-        return $this->callApi($this->config['gpt']['api_url'], $this->config['gpt']['api_key'], $data);
-    }
-
-    /**
-     * 文升文模型
-     */
-    public function TxtGptApi($prompt)
-    {
-        $data = [
-            'prompt' => $prompt,
-            'model' => 'gpt-4',
-            'session_id' => null,
-            'context_reset' => true
+            'update_time' => date('Y-m-d H:i:s')
         ];
-
-        return $this->callApi($this->config['gpt']['api_url'],$this->config['gpt']['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}");
+        Db::name('text_to_image')->insert($record);
+        return ;
     }
-
 }

+ 48 - 144
application/job/TextToImageJob.php

@@ -1,30 +1,17 @@
 <?php
-
 namespace app\job;
-
+use app\service\AIGatewayService;
 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)
     {
+
         $logId = $data['log_id'] ?? null;
 
         try {
@@ -46,10 +33,11 @@ class TextToImageJob
                 ]);
             }
 
-            $fullPath = rtrim($data['sourceDir'], '/') . '/' . ltrim($data['file_name'], '/');
+            //文件路径 + 图片名称
+            $old_image_url = rtrim($data['sourceDir'], '/') . '/' . ltrim($data['file_name'], '/');
 
             $list = Db::name("text_to_image")
-                ->where('old_image_url', $fullPath)
+                ->where('old_image_url', $old_image_url)
                 ->where('img_name', '<>', '')
                 ->where('status', 0)
                 ->select();
@@ -88,7 +76,7 @@ class TextToImageJob
                 if ($logId) {
                     Db::name('image_task_log')->where('id', $logId)->update([
                         'status' => 2,
-                        'log' => '文生图执行成功',
+                        'log' => '文生图处理成功',
                         'update_time' => date('Y-m-d H:i:s')
                     ]);
                 }
@@ -106,6 +94,17 @@ class TextToImageJob
                 }
             }
 
+            // 如果还有链式任务,继续推送
+            if (!empty($data['chain_next'])) {
+                $nextType = array_shift($data['chain_next']);
+                $data['type'] = $nextType;
+
+                Queue::push('app\job\ImageArrJob', [
+                    'task_id' => $data['task_id'],
+                    'data' => [$data]
+                ], 'arrimage');
+            }
+
             $job->delete();
 
         } catch (\Exception $e) {
@@ -151,8 +150,6 @@ class TextToImageJob
             }
         }
 
-
-
         // 查询数据库记录
         $record = Db::name('text_to_image')
             ->where('old_image_url', 'like', "%{$fileName}")
@@ -183,16 +180,39 @@ class TextToImageJob
                 echo "🚫 跳过生成:提示词中包含关键词“{$keyword}”,记录 ID:{$skipId}\n";
                 $updateRes = Db::name('text_to_image')->where('id', $skipId)->update([
                     'status' => 3,
+                    'error_msg' => "提示词中包含关键词".$keyword,
                     'update_time' => date('Y-m-d H:i:s')
                 ]);
+
+                // 1. 获取 text_to_image 表中指定 ID 的记录
+                $one_id = Db::name('text_to_image')->where('id', $skipId)->find();
+                if ($one_id && isset($one_id['old_image_url'])) {
+                    // 2. 查询 image_task_log 中 file_name 匹配的记录,按 id 降序排序,取最后一条
+                    $lastLog = Db::name('image_task_log')
+                        ->where('file_name', $one_id['old_image_url'])
+                        ->order('id', 'desc')
+                        ->find();
+                    if ($lastLog) {
+                        // 3. 执行更新
+                        Db::name('image_task_log')
+                            ->where('id', $lastLog['id'])
+                            ->update([
+                                'model_name'  => "提示词中包含关键词" . $keyword,
+                                'update_time' => date('Y-m-d H:i:s')
+                            ]);
+                    }
+                }
+
                 return "跳过生成:记录 ID {$skipId},包含关键词 - {$keyword}";
             }
         }
 
         //文生图调用
-        $dalle1024 = $this->callDalleApi($prompt,$selectedOption);
+//        $dalle1024 = $this->callDalleApi($prompt,$selectedOption);
+        $ai = new AIGatewayService();
+        $dalle1024 = $ai->callDalleApi($prompt,$selectedOption);
 
-        //查询接口调用时长
+        //检测查询接口调用时长
         $endTime = microtime(true);
         $executionTime = $endTime - $startTime;
         echo "API调用耗时: " . round($executionTime, 3) . " 秒\n";
@@ -265,127 +285,11 @@ class TextToImageJob
             'quality' => 'hd',
             'style' => 'vivid',
             'size' => "{$width}x{$height}",
-            'updated_time' => date('Y-m-d H:i:s'),
-            'status' => $status
+            'update_time' => date('Y-m-d H:i:s'),
+            'status' => $status,
+            'status_name' => "文生图"
         ]);
         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,$selectedOption)
-    {
-        if($selectedOption == 'dall-e-3'){
-            $data = [
-                'prompt' => $prompt,
-                'model'   => $selectedOption,
-                'n'       => 1,
-                'size'    => '1024x1024',
-                'quality' => 'standard',
-                'style'   => 'vivid',
-                'response_format' => 'url',
-                'session_id' => null,
-                'context_reset' => true
-            ];
-        }else{
-            $data = [
-                'prompt' => $prompt,
-                'model'   => $selectedOption,
-                'n'       => 1,
-                'size'    => '1024x1024',
-                'quality' => 'hd',
-                'style'   => 'vivid',
-                'response_format' => 'url',
-                'session_id' => null,
-                'context_reset' => true
-            ];
-        }
-
-        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}");
-    }
-
-
 }

+ 22 - 77
application/job/TextToTextJob.php

@@ -1,26 +1,17 @@
 <?php
 namespace app\job;
+use app\service\AIGatewayService;
 use think\Db;
 use think\queue\Job;
 use think\Queue;
+/**
+ * 文生文队列任务
+ */
 class TextToTextJob
 {
-    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)
     {
+
         $logId = $data['log_id'] ?? null;
         echo "━━━━━━━━━━ ▶ 文生文任务开始处理━━━━━━━━━━\n";
 
@@ -61,7 +52,7 @@ class TextToTextJob
                 if ($logId) {
                     Db::name('image_task_log')->where('id', $logId)->update([
                         'status' => 2,
-                        'log' => '文生文执行成功',
+                        'log' => '文生文处理成功',
                         'update_time' => date('Y-m-d H:i:s')
                     ]);
                 }
@@ -79,6 +70,17 @@ class TextToTextJob
                 }
             }
 
+            // 🔁 链式执行:检查是否还有下一个任务
+            if (!empty($data['chain_next'])) {
+                $nextType = array_shift($data['chain_next']); // 获取下一个任务类型
+                $data['type'] = $nextType;
+
+                Queue::push('app\job\ImageArrJob', [
+                    'task_id' => $data['task_id'],
+                    'data' => [$data]  // 继续传一个任务
+                ], 'arrimage');
+            }
+
         } catch (\Exception $e) {
             echo "❌ 错误信息: " . $e->getMessage() . "\n";
             echo "📄 文件: " . $e->getFile() . ",第 " . $e->getLine() . " 行\n";
@@ -113,74 +115,17 @@ class TextToTextJob
         if (!$record) {return '没有找到匹配的图像记录';}
 
         // 调用文生文
-        $gptRes = $this->TxtGptApi($template['english_content'].$record['english_description']);
+//        $gptRes = $this->TxtGptApi($template['english_content'].$record['english_description']);
+        $ai = new AIGatewayService();
+        $gptRes = $ai->txtGptApi($template['english_content'].$record['english_description']);
         $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
 
         // 更新数据库记录
         Db::name('text_to_image')->where('id', $record['id'])->update([
-            'english_description' => $gptText
+            'english_description' => $gptText,
+            'status_name' => "文生文"
         ]);
         return 0;
     }
 
-    /**
-     * 文升文模型
-     */
-    public function TxtGptApi($prompt)
-    {
-        $data = [
-            'prompt' => $prompt,
-            'model' => 'gpt-4',
-            'session_id' => null,
-            'context_reset' => true
-        ];
-
-        return $this->callApi($this->config['gpt']['api_url'],$this->config['gpt']['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}");
-    }
-
 }

+ 200 - 0
application/service/AIGatewayService.php

@@ -0,0 +1,200 @@
+<?php
+namespace app\service;
+use think\Db;
+use think\Queue;
+class AIGatewayService{
+
+    /**
+     * 接口访问配置
+     *
+     * 配置说明:
+     * - gpt:用于文本生成或图文识别模型接口(如 chat/completions)
+     * OpenAI GPT 接口配置:支持 chat、图文结合模型(如 gpt-4-vision-preview)
+     *
+     * - dalle:用于文生图(图像生成)模型接口
+     * OpenAI DALL·E 接口配置:用于文生图像(图像生成)
+     *
+     * 每个模块包含:
+     * - api_key:API 调用密钥(Bearer Token)
+     * - api_url:对应功能的服务端地址
+     */
+    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'
+        ]
+    ];
+
+    /**
+     * 调用 GPT-4 图文识别模型接口(图生文)
+     *
+     * @param string $imageUrl 图像 URL,支持公网可访问地址
+     * @param string $prompt   对图像的提问内容或提示文本
+     *
+     * 功能说明:
+     * - 使用 OpenAI 的 gpt-4-vision-preview 模型对图片进行图文理解
+     * - 支持图像与文本混合输入,返回图像内容相关的文本输出
+     * - 限制最大 token 数为 1000
+     *
+     * 返回值:
+     * - 返回调用 GPT API 后的响应结果(通常为模型生成的文本)
+     */
+    public function callGptApi($imageUrl, $prompt)
+    {
+        $data = [
+            "model" => "gpt-4-vision-preview",
+            "messages" => [[
+                "role" => "user",
+                "content" => [
+                    ["type" => "text", "text" => $prompt],
+                    ["type" => "image_url", "image_url" => [
+                        "url" => $imageUrl,
+                        "detail" => "auto"
+                    ]]
+                ]
+            ]],
+            "max_tokens" => 1000
+        ];
+        return $this->callApi($this->config['gpt']['api_url'], $this->config['gpt']['api_key'], $data);
+    }
+
+    /**
+     * 调用 GPT 文生文模型接口(文本生成)
+     *
+     * @param string $prompt 用户输入的文本提示内容
+     *
+     * 功能说明:
+     * - 使用 OpenAI 的 GPT-4 模型,根据用户提供的 prompt 文本生成响应内容
+     * - 支持上下文重置,确保每次调用为独立会话(无历史记忆)
+     * - session_id 设为 null 表示不使用会话追踪
+     *
+     * 返回值:
+     * - 返回调用 GPT 接口后的响应结果(通常为模型生成的文本)
+     */
+    public function txtGptApi($prompt)
+    {
+        $data = [
+            'prompt' => $prompt,
+            'model' => 'gpt-4',
+            'session_id' => null,
+            'context_reset' => true
+        ];
+        return $this->callApi(
+            $this->config['gpt']['api_url'],
+            $this->config['gpt']['api_key'],
+            $data
+        );
+    }
+
+    /**
+     * 调用 DALL·E 图像生成接口(文生图)
+     *
+     * @param string $prompt          提示文本,用于指导图像生成
+     * @param string $selectedOption  模型名称,例如 'dall-e-3' 或其他兼容模型
+     *
+     * 功能说明:
+     * - 根据用户提供的文本提示,通过指定的模型生成图像
+     * - 若选择模型为 'dall-e-3',使用标准质量(standard);其他模型使用高清质量(hd)
+     * - 默认生成 1 张图像,尺寸为 1024x1024,风格为 vivid(生动)
+     * - 返回的图像链接格式为 URL
+     * - 每次调用为独立会话(session_id 为 null,context_reset 为 true)
+     *
+     * 返回值:
+     * - 返回调用 DALL·E 接口后的响应结果(图像 URL)
+     */
+    public function callDalleApi($prompt,$selectedOption)
+    {
+        if($selectedOption == 'dall-e-3'){
+            $data = [
+                'prompt' => $prompt,
+                'model'   => $selectedOption,
+                'n'       => 1,
+                'size'    => '1024x1024',
+                'quality' => 'standard',
+                'style'   => 'vivid',
+                'response_format' => 'url',
+                'session_id' => null,
+                'context_reset' => true
+            ];
+        }else{
+            $data = [
+                'prompt' => $prompt,
+                'model'   => $selectedOption,
+                'n'       => 1,
+                'size'    => '1024x1024',
+                'quality' => 'hd',
+                'style'   => 'vivid',
+                'response_format' => 'url',
+                'session_id' => null,
+                'context_reset' => true
+            ];
+        }
+
+        return $this->callApi($this->config['dalle']['api_url'], $this->config['dalle']['api_key'], $data);
+    }
+
+    /**
+     * 通用 API 调用方法(支持重试机制)
+     *
+     * @param string $url     接口地址
+     * @param string $apiKey  授权密钥(Bearer Token)
+     * @param array  $data    请求数据(JSON 格式)
+     *
+     * 功能说明:
+     * - 使用 cURL 发送 POST 请求到指定 API 接口
+     * - 设置请求头和超时时间等参数
+     * - 支持最多重试 2 次,当接口调用失败时自动重试
+     * - 返回成功时解析 JSON 响应为数组
+     *
+     * 异常处理:
+     * - 若全部重试失败,将抛出异常并包含最后一次错误信息
+     *
+     * @return array 接口响应数据(成功时返回解析后的数组)
+     * @throws \Exception 接口请求失败时抛出异常
+     */
+    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}");
+    }
+}

+ 91 - 70
application/service/ImageService.php

@@ -9,6 +9,9 @@ use think\Queue;
 class ImageService{
     /**
      * 处理图像并推送到队列中
+     * sourceDir 源目录uploads/operate/ai/Preview/
+     * outputDir 输出目录/uploads/operate/ai/dall-e/hua/
+     * file_name 文件名0194b6fdd6203fda369d5e3b74b6b454.png
      */
 
     public function handleImage($params) {
@@ -18,21 +21,25 @@ class ImageService{
         $batch = $params["batch"]; // 获取图像批量信息
         $num = $params["num"]; // 获取需要生成的实例数量
 
+        // 获取模板
+        // english_content 文生文提示词
+        // content 图生文提示词
+        $template = Db::name('template')
+            ->field('id,english_content,content,ids')
+            ->where('ids',1)
+            ->find();
+
         // 遍历每个图像,进行处理
         foreach ($batch as $k => $v) {
-            $template = Db::name('template')
-                ->field('id,english_content')
-                ->where('ids',1)
-                ->find();
             $baseItem = [
-                "sourceDir" => $this->sourceDir($v, 1), // 获取源目录
-                "outputDir" => $this->sourceDir($v, 2), // 获取输出目录
-                "file_name" => $this->sourceDir($v, 3), // 获取文件名
+                "sourceDir" => $this->sourceDir($v, 1), //
+                "outputDir" => $this->sourceDir($v, 2), //
+                "file_name" => $this->sourceDir($v, 3), //
                 "type" => $params['type'] ?? '', // 获取处理类型
-                "selectedOption" => $params['selectedOption'], //生图模型参数
-                "prompt" => $template['content'], // 获取处理提示
-                "width" => $params['width'], // 获取图像宽度
-                "height" => $params['height'] // 获取图像高度
+                "selectedOption" => $params['selectedOption'], //生图模型参数
+                "prompt" => $template['content'],
+                "width" => $params['width'],
+                "height" => $params['height']
             ];
             // 创建$num个相同的项目并合并到$arr
             $arr = array_merge($arr, array_fill(0, $num, $baseItem));
@@ -48,42 +55,65 @@ class ImageService{
             'params'       => json_encode($params, JSON_UNESCAPED_UNICODE)
         ];
 
-        // 根据任务类型设定模型
-        switch ($params['type']) {
-            case '图生文':
-                $insertData['model'] = 'gpt-4-vision-preview';
-                break;
+        if (empty($params['type'])) {
+            // 未指定类型,执行链式任务
+            $insertData['model'] = "gpt-4-vision-preview,"."gpt-4,".$params['selectedOption'];
+            $insertData['model_name'] = '文生图';
 
-            case '文生文':
-                $insertData['model'] = 'gpt-4';
-                break;
+//            echo "<pre>";print_r($insertData);echo "<pre>";die;
 
-            case '文生图':
-                $insertData['model'] = $params['selectedOption'] ?? '未知模型';
-                break;
+            $task_id = Db::name('queue_logs')->insertGetId($insertData);
 
-            default:
-                // 混合任务或自定义模型逻辑
-                $selected = $params['selectedOption'] ?? '未知';
-                $insertData['model'] = "gpt-4-vision-preview,gpt-4,{$selected}";
-                break;
-        }
+            $arr = array_map(function ($item) use ($task_id) {
+                $item['type'] = '图生文';
+                $item['chain_next'] = ['文生文', '文生图'];
+                $item['task_id'] = $task_id;
+                return $item;
+            }, $arr);
 
-        // 插入并获取任务 ID
-        $task_id = Db::name('queue_logs')->insertGetId($insertData);
+            $payload = [
+                'task_id' => $task_id,
+                'data' => $arr
+            ];
 
+            Queue::push('app\job\ImageArrJob', $payload, "arrimage");
+        } else {
 
-        // 推送到队列
-        $payload = [
-            'task_id' => $task_id,
-            'data' => $arr
-        ];
+            // 指定了单一任务
+            switch ($params['type']) {
+                case '图生文':
+                    $insertData['model'] = 'gpt-4-vision-preview';
+                    $insertData['model_name'] = '图生文';
+                    break;
+                case '文生文':
+                    $insertData['model'] = 'gpt-4';
+                    $insertData['model_name'] = '文生文';
+                    break;
+                case '文生图':
+                    $insertData['model'] = $params['selectedOption'];
+                    $insertData['model_name'] = '文生图';
+                    break;
+                default:
+                    return false;
+            }
 
-        //测试查看推送队列前的数据
-//        echo "<pre>";print_r($payload);echo "<pre>";die;
+            $task_id = Db::name('queue_logs')->insertGetId($insertData);
 
-        // 推送队列
-        Queue::push('app\job\ImageArrJob', $payload, "arrimage");
+            $arr = array_map(function ($item) use ($params, $task_id) {
+                $item['type'] = $params['type'];
+                $item['task_id'] = $task_id;
+                return $item;
+            }, $arr);
+
+            $payload = [
+                'task_id' => $task_id,
+                'data' => $arr
+            ];
+
+            Queue::push('app\job\ImageArrJob', $payload, "arrimage");
+        }
+
+        return true;
     }
 
     /**
@@ -93,48 +123,39 @@ class ImageService{
      * @param int $type 返回类型标识
      * @return string|null 返回解析后的路径或文件名
      */
-    public function sourceDir($filePath,$type){
+    public function sourceDir($filePath, $type) {
         $arr = [];
 
-        // 使用正则表达式匹配完整路径
-        if (preg_match('/^(.+?)\/Preview\/(\d{8})\/(.+)$/', $filePath, $matches)) {
-            $arr =  [
-                'basePath' => $matches[1],  // 基础路径
-                'date' => $matches[2],      // 日期 (YYYYMMDD)
-                'filename' => $matches[3]   // 文件名
-            ];
-        }else{
-            // 备用方案:如果正则匹配失败
-            $pathParts = explode('/', $filePath);
-            $filename = array_pop($pathParts);
+        $pathParts = explode('/', $filePath);
+        $filename = array_pop($pathParts); // 最后是文件名
 
-            $date = '';
-            $baseParts = [];
+        $baseParts = $pathParts;
 
-            foreach ($pathParts as $part) {
-                if (preg_match('/^\d{8}$/', $part)) {
-                    $date = $part;
-                    break;
-                }
-                $baseParts[] = $part;
+        // 查找是否有 8 位数字(即日期)文件夹
+        $date = '';
+        foreach ($pathParts as $index => $part) {
+            if (preg_match('/^\d{8}$/', $part)) {
+                $date = $part;
+                unset($baseParts[$index]); // 日期不算在 basePath 里
+                break;
             }
-
-            $arr = [
-                'basePath' => implode('/', $baseParts),
-                'date' => $date,
-                'filename' => $filename
-            ];
         }
 
-        // 根据类型返回不同的路径
+        $arr = [
+            'basePath' => implode('/', $baseParts),
+            'date' => $date,
+            'filename' => $filename
+        ];
+
+        // 根据类型返回不同路径
         if ($type == 1) {
-            return $arr["basePath"] . "/Preview/" . $arr["date"]; // 返回预览目录
+            return $arr["basePath"]; // 只返回基础路径
         }
         if ($type == 2) {
-            return '/' . $arr["basePath"] . "/dall-e/" . $arr["date"]; // 返回输出目录
+            return '/' . str_replace('/Preview/', '/dall-e/', $arr["basePath"]) . $arr["date"];
         }
-        if($type==3){
-            return $arr["filename"]; // 返回图片名称
+        if ($type == 3) {
+            return $arr["filename"];
         }
     }
 }