| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657 |
- <?php
- namespace app\api\controller;
- use app\common\controller\Api;
- use app\job\ImageJob;
- use app\service\ImageService;
- use think\App;
- use think\Db;
- use think\Log;
- use think\Queue;
- use think\queue\job\Redis;
- class WorkOrder extends Api
- {
- protected $noNeedLogin = ['*'];
- protected $noNeedRight = ['*'];
- /**
- * 出图接口
- */
- public function imageToText()
- {
- $params = $this->request->param();
- $service = new ImageService();
- $service->handleImage($params);
- $this->success('成功存入队列中');
- }
- /**
- * 开启队列任务
- */
- public function kaiStats()
- {
- // 判断是否已有监听进程在运行
- $check = shell_exec("ps aux | grep 'queue:listen' | grep -v grep");
- if ($check) {
- return json([
- 'code' => 1,
- 'msg' => '监听进程已存在,请勿重复启动'
- ]);
- }
- // 启动监听
- // $command = 'php think queue:listen --queue --timeout=300 --sleep=3 --memory=256 > /dev/null 2>&1 &';
- $command = 'nohup php think queue:listen --queue --timeout=300 --sleep=3 --memory=256 > /var/log/job_queue.log 2>&1 &';
- exec($command, $output, $status);
- if ($status === 0) {
- return json([
- 'code' => 0,
- 'msg' => '队列监听已启动'
- ]);
- } else {
- return json([
- 'code' => 1,
- 'msg' => '队列启动失败',
- 'output' => $output
- ]);
- }
- }
- /**
- * 查看队列任务
- */
- public function queueStats()
- {
- $statusCounts = Db::name('queue_log')
- ->field('status, COUNT(*) as total')
- ->whereTime('created_at', 'today')
- ->where('status','<>', 4)
- ->group('status')
- ->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
- ]);
- }
- /**
- * 清空队列(同时删除近30分钟的队列日志)
- */
- public function stopQueueProcesses()
- {
- $redis = new \Redis();
- $redis->connect('127.0.0.1', 6379);
- $redis->select(15);
- $key = 'queues:txttoimg';
- $key1 = 'queues:imgtotxt';
- $count = $redis->lLen($key);
- $count1 = $redis->lLen($key1);
- $count = $count+$count1;
- // 计算时间:当前时间前30分钟
- $time = date('Y-m-d H:i:s', strtotime('-30 minutes'));
- if ($count === 0) {
- // 删除数据库中状态为0且 created_at 在最近30分钟的数据
- $deleteCount = Db::name('queue_log')
- ->where('created_at', '>=', $time)
- ->delete();
- return json([
- 'code' => 1,
- 'msg' => '暂无队列需要停止'
- ]);
- }
- // 清空 Redis 队列
- $redis->del($key);
- $redis->del($key1);
- // 删除数据库中状态为0且 created_at 在最近30分钟的数据
- $deleteCount = Db::name('queue_log')
- ->where('created_at', '>=', $time)
- ->delete();
- return json([
- 'code' => 0,
- 'msg' => '已成功停止队列任务'
- ]);
- }
- /**
- * 显示当前运行中的队列监听进程
- */
- public function viewQueueStatus()
- {
- $redis = new \Redis();
- $redis->connect('127.0.0.1', 6379);
- $redis->select(15);
- $key = 'queues:imgtotxt';
- $count = $redis->lLen($key);
- $list = $redis->lRange($key, 0, 9);
- $parsed = array_map(function ($item) {
- return json_decode($item, true);
- }, $list);
- return json([
- 'code' => 0,
- 'msg' => '查询成功',
- 'count' => $count,
- 'tasks_preview' => $parsed
- ]);
- }
- //单个调用[可以用来做测试]
- // 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]);
- // }
- }
|