liuhairui 5 hours ago
parent
commit
f39bbbae55

+ 77 - 878
application/api/controller/Index.php

@@ -23,938 +23,137 @@ class Index extends Api
         $this->success('请求成功');
     }
 
-    /**
-     * 图生文接口
-     */
-    public function img_to_txt() {
-        $params = $this->request->param();
-
-        $prompt = trim($params['prompt']);
-        $old_path = trim($params['path']);
-        $model = trim($params['model']);
-        $status_val = trim($params['status_val']);
-
-        // 参数验证
-        if (empty($prompt) || empty($old_path) || empty($model)) {
-            return json([
-                'code' => 1,
-                'msg' => '缺少必要参数',
-                'data' => null
-            ]);
-        }
-
-        $aiGateway = new AIGatewayService();
-
-        // 获取图片的base64数据和MIME类型
-        $imageData = AIGatewayService::file_get_contents($old_path);
-        $base64Data = $imageData['base64Data'];
-        $mimeType = $imageData['mimeType'];
-
-        $formattedPrompt = "1. 输出语言:所有内容必须为纯简体中文,禁止出现任何英文、拼音、数字、注释、解释性文字、引导语、示例、标点外的特殊符号;
-            2. 第一步(提取原图产品):
-               - 用1句完整中文描述原图产品,字数控制在50字以内;
-               - 必须包含「主体细节、产品名称、商标、类型、颜色、风格」核心;
-            3. 第二步(生成新图提示词):
-               - 仅替换【模板提示词】中「产品主体」相关内容为第一步的产品描述;
-               - 严格保留模板中「设计风格、光影、背景、比例、排版」等非产品相关信息;
-               - 完全排除模板中的标题类信息,仅保留提示词核心内容;
-               - 替换后必须完全保留原图产品的核心特征,禁止修改模板非产品核心信息;
-            4. 输出格式:不允许添加任何解释、引导、说明、示例、备注,仅返回「产品描述 + 替换后提示词」,直接输出纯文本;
-            【模板提示词】{$prompt}";
-
-        $result = $aiGateway->callGptApi($model, $formattedPrompt, $mimeType, $base64Data);
-
-        // Gemini模型响应格式处理
-        $imgToTxtContent = $result['candidates'][0]['content']['parts'][0]['text'];
-
-        // // 图生文成功后,调用文生文功能(gemini-2.0-flash)
-        $txtToTxtPrompt = "转换成中文格式,去掉其他特殊符号,不允许添加任何解释、引导、说明、示例等文字:\n\n{$imgToTxtContent}";
-        $txtToTxtResult = $aiGateway->txtGptApi($txtToTxtPrompt, 'gemini-2.0-flash');
-        $finalContent = $txtToTxtResult['candidates'][0]['content']['parts'][0]['text'];
-
-        // 处理调试输出内容
-        $debugContent = json_encode($finalContent, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
-        // 去掉特殊符号(只保留字母、数字、中文、常用标点和换行)
-        $debugContent = preg_replace('/[^\p{Han}\w\s\n\r\.,,。!!??\-\::;;]/u', '', $debugContent);
-        // 去掉第一个冒号前的文字
-        $debugContent = preg_replace('/^[^:]+:/', '', $debugContent);
-
-        return json([
-            'code' => 0,
-            'msg' => '处理成功',
-            'data' => [
-                'content' => $debugContent,
-                'english_content' => $imgToTxtContent
-            ]
-        ]);
-
-    }
+    // 向量引擎配置
+    private $baseUrl = "https://api.vectorengine.ai/v1";
+    private $apiKey = "sk-P877pnXMk2erRS2an7qEa3Kdb3rIb7JVAWZ39lhA8HeN71gZ"; // 从控制台获取
+    private $timeout = 120; // 超时时间(秒),视频生成需要更长
 
     /**
      * 文生图接口
+     * POST /api/index/textToImage
+     * 参数: prompt (string) 提示词
      */
-    public function txt_to_img() {
+    public function textToImage()
+    {
         $params = $this->request->param();
-
-        $prompt = trim($params['prompt']);
-        $model = trim($params['model']);
-        $status_val = trim($params['status_val']);
-        $size = trim($params['size']);
-
-        // 获取产品信息
-        $product = Db::name('product')->where('id', 1)->find();
-        if (empty($product)) {
-            return json([
-                'code' => 1,
-                'msg' => '产品不存在',
-                'data' => null
-            ]);
+        if (empty($params)) {
+            $this->error('提示词不能为空');
         }
-        $product_code = $product['product_code'];
-        $product_code_prefix = substr($product_code, 0, 9); // 前九位
-
-        // 构建URL路径(使用正斜杠)
-        $url_path = '/uploads/merchant/' . $product_code_prefix . '/' . $product_code . '/newimg/';
-        // 构建物理路径(使用正斜杠确保统一格式)
-        $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'merchant' . '/' . $product_code_prefix . '/' . $product_code . '/' . 'newimg' . '/';
-        // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
-        $save_path = str_replace('\\', '/', $save_path);
-        // 自动创建文件夹(如果不存在)
-        if (!is_dir($save_path)) {
-            mkdir($save_path, 0755, true);
-        }
-
-        // 调用AI生成图片
-        $aiGateway = new AIGatewayService();
-        $res = $aiGateway->callDalleApi($prompt, $model, $size);
-
-        // 提取base64图片数据
-        if (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
-            $text_content = $res['candidates'][0]['content']['parts'][0]['text'];
-            // 匹配base64图片数据
-            preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $text_content, $matches);
-            if (empty($matches)) {
-                return json([
-                    'code' => 1,
-                    'msg' => '未找到图片数据',
-                    'data' => null
-                ]);
-            }
-            $image_type = $matches[1];
-            $base64_data = $matches[2];
-
-            // 解码base64数据
-            $image_data = base64_decode($base64_data);
-            if ($image_data === false) {
-                return json([
-                    'code' => 1,
-                    'msg' => '图片解码失败',
-                    'data' => null
-                ]);
-            }
-
-            // 生成唯一文件名(包含扩展名)
-            $file_name = uniqid() . '.' . $image_type;
-            $full_file_path = $save_path . $file_name;
-
-            // 保存图片到文件系统
-            if (!file_put_contents($full_file_path, $image_data)) {
-                return json([
-                    'code' => 1,
-                    'msg' => '图片保存失败',
-                    'data' => null
-                ]);
-            }
-            // 生成数据库存储路径(使用正斜杠格式)
-            $db_img_path = $url_path . $file_name;
 
-            Db::name('product')->where('id', 1)->update(['product_new_img' => $db_img_path]);
+        $data = [
+            "model"  => "gemini-3-pro-image-preview", // 可替换为 dall-e-3 等
+            "prompt" => $params['prompt'],
+            "n"      => 1,
+            "size"   => "1024x1024"
+        ];
 
-            return json([
-                'code' => 0,
-                'msg' => '文生图请求成功并保存图片',
-                'data' => [
-                    'img' => $db_img_path,
-                    'product_id' => 1
-                ]
-            ]);
+        $result = $this->requestVectorEngine("/images/generations", $data);
+        if ($result['code'] === 0) {
+            $this->success('生成成功', $result['data']);
         } else {
-            return json([
-                'code' => 1,
-                'msg' => 'AI返回格式错误',
-                'data' => null
-            ]);
+            $this->error($result['msg'], $result['data']);
         }
     }
 
-
     /**
-     * 图生图:gemini-3-pro-image-preview
-     * 产品图 + 参考模板图 + 提示词 → 生成新图
-     * 接口访问路径:http://mes-ai-api:9091/index.php/api/Index/img_to_img(POST请求)
-     * 参数:product_img, template_img(图片路径如 /uploads/merchant/xxx/xxx.png),prompt(可选),model(可选)
+     * 图生文接口
+     * POST /api/index/imageToText
+     * 参数: image_url (string) 公网图片URL, prompt (string) 提问指令
      */
-    public function img_to_img()
+    public function imageToText()
     {
-        try {
-            // ========== 1. 基础配置 ==========
-            $apiUrl = 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:generateContent';
-            $apiKey = 'sk-IrIWvqkTs8DwvB9MFBRSWKQHdZRawNeKTnVPHjAJ0KryBWeF';
-
-            // ========== 2. 获取请求参数(兼容 $_REQUEST,与本地测试脚本一致)==========
-            $params = array_merge($_REQUEST, $this->request->param());
-            $productImgRaw = trim($params['product_img'] ?? '/uploads/merchant/690377511/6903775111138/oldimg/伊利牛奶.png');
-            $templateImgRaw = trim($params['template_img'] ?? '/uploads/template/2026-03-06/69aa45a34161f_20260306111027.png');
-            $customPrompt = trim($params['prompt'] ?? '');
-            if (empty($productImgRaw) || empty($templateImgRaw)) {
-                $this->json_response(['code' => 1, 'msg' => '缺少必要参数:product_img、template_img', 'data' => null]);
-            }
-
-        // 将 /uploads/xxx 或 uploads/xxx 转为本地绝对路径
-        $productImgPath = $this->resolveImagePath($productImgRaw);
-        $templateImgPath = $this->resolveImagePath($templateImgRaw);
-
-        // ========== 3. 提示词 ==========
-        $prompt = $customPrompt ?: '请完成产品模板替换:
-        1. 从产品图提取产品主体、品牌名称、核心文案;
-        2. 从模板图继承版式布局、文字排版、色彩风格、背景元素;
-        3. 将模板图中的产品和文字替换为产品图的内容;
-        4. 最终生成的图片与模板图视觉风格100%统一,仅替换产品和文字。';
+        $imageUrl = $this->request->post('image_url');
+        $prompt   = $this->request->post('prompt', '描述这张图片的内容');
 
-        // ========== 4. 图片转Base64 ==========
-        $productImg = $this->img_to_base64($productImgPath);
-        if (isset($productImg['error'])) {
-            $this->json_response(['code' => 1, 'msg' => '[步骤1]产品图加载失败:' . $productImg['error'], 'data' => ['path' => $productImgRaw]]);
+        if (empty($imageUrl)) {
+            $this->error('图片URL不能为空');
         }
 
-        $templateImg = $this->img_to_base64($templateImgPath);
-        if (isset($templateImg['error'])) {
-            $this->json_response(['code' => 1, 'msg' => '[步骤2]模板图加载失败:' . $templateImg['error'], 'data' => ['path' => $templateImgRaw]]);
-        }
-
-        // ========== 5. 构造请求参数 ==========
-        $requestData = [
-            'contents' => [
+        $data = [
+            "model"    => "gpt-4-vision-preview",
+            "messages" => [
                 [
-                    'role' => 'user',
-                    'parts' => [
-                        ['text' => $prompt],
-                        ['inlineData' => ['mimeType' => $productImg['mime'], 'data' => $productImg['base64']]],
-                        ['inlineData' => ['mimeType' => $templateImg['mime'], 'data' => $templateImg['base64']]]
+                    "role"    => "user",
+                    "content" => [
+                        ["type" => "text", "text" => $prompt],
+                        ["type" => "image_url", "image_url" => ["url" => $imageUrl]]
                     ]
                 ]
             ],
-            'generationConfig' => [
-                'responseModalities' => ['IMAGE'],
-                'imageConfig' => [
-                    'aspectRatio' => '5:4',
-                    'quality' => 'HIGH',
-                    'width' => 1000,
-                    'height' => 800
-                ],
-                'temperature' => 0.3,
-                'topP' => 0.8,
-                'maxOutputTokens' => 2048
-            ]
+            "max_tokens" => 1000
         ];
 
-        // ========== 6. 发起CURL请求 ==========
-        $ch = curl_init();
-        curl_setopt_array($ch, [
-            CURLOPT_URL => $apiUrl,
-            CURLOPT_RETURNTRANSFER => true,
-            CURLOPT_POST => true,
-            CURLOPT_POSTFIELDS => json_encode($requestData, JSON_UNESCAPED_UNICODE),
-            CURLOPT_HTTPHEADER => [
-                'Content-Type: application/json',
-                'Authorization: Bearer ' . $apiKey
-            ],
-            CURLOPT_TIMEOUT => 300,
-            CURLOPT_SSL_VERIFYPEER => false,
-            CURLOPT_SSL_VERIFYHOST => false
-        ]);
-
-        $response = curl_exec($ch);
-        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-        $curlErr = curl_error($ch);
-        curl_close($ch);
-
-        // ========== 7. 错误处理 ==========
-        if ($curlErr) {
-            $this->json_response(['code' => 1, 'msg' => '[步骤3]CURL请求失败:' . $curlErr, 'data' => null]);
-        }
-        if ($httpCode != 200) {
-            $errDetail = is_string($response) ? substr($response, 0, 800) : json_encode($response);
-            $this->json_response(['code' => 1, 'msg' => '[步骤4]API调用失败 HTTP' . $httpCode . ',可能原因:API Key过期/无效、配额不足、请求格式错误。响应:' . $errDetail, 'data' => null]);
-        }
-
-        // ========== 8. 解析响应 ==========
-        $result = json_decode($response, true);
-        if (json_last_error() !== JSON_ERROR_NONE) {
-            $this->json_response(['code' => 1, 'msg' => '[步骤5]API响应非JSON格式:' . json_last_error_msg(), 'data' => ['raw' => substr($response, 0, 300)]]);
-        }
-
-        // ========== 9. 提取图片数据 ==========
-        $base64Data = null;
-        $imageType = 'png';
-        if (isset($result['candidates'][0]['content']['parts'][0]['inlineData']['data'])) {
-            $base64Data = $result['candidates'][0]['content']['parts'][0]['inlineData']['data'];
-        } elseif (isset($result['candidates'][0]['content']['parts'][0]['text'])) {
-            $text = $result['candidates'][0]['content']['parts'][0]['text'];
-            if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,([^\s"\']+)/i', $text, $m)) {
-                $imageType = $m[1];
-                $base64Data = $m[2];
-            }
-        }
-
-        if (!$base64Data) {
-            $errMsg = isset($result['error']['message']) ? $result['error']['message'] : '响应结构异常';
-            $this->json_response(['code' => 1, 'msg' => '[步骤6]未获取到图片数据。' . $errMsg . '。完整响应见data', 'data' => $result]);
-        }
-
-        // ========== 10. 保存图片到 uploads/ceshi/ ==========
-        $imageData = base64_decode($base64Data);
-        if ($imageData === false || strlen($imageData) < 100) {
-            $this->json_response(['code' => 1, 'msg' => '[步骤7]图片Base64解码失败', 'data' => null]);
-        }
-
-        $saveDir = str_replace('\\', '/', ROOT_PATH . 'public/uploads/ceshi/');
-        if (!is_dir($saveDir)) {
-            mkdir($saveDir, 0755, true);
-        }
-        $fileName = 'img2img-' . date('YmdHis') . '-' . uniqid() . '.' . $imageType;
-        $fullPath = $saveDir . $fileName;
-        if (!file_put_contents($fullPath, $imageData)) {
-            $this->json_response(['code' => 1, 'msg' => '[步骤8]图片保存失败,请检查目录权限:' . $saveDir, 'data' => null]);
-        }
-
-        // ========== 11. 返回成功响应(返回可访问的Web路径) ==========
-        $webPath = '/uploads/ceshi/' . $fileName;
-        $this->json_response([
-            'code' => 0,
-            'msg' => '图生图成功',
-            'data' => ['image' => $webPath]
-        ]);
-        } catch (\Exception $e) {
-            $this->json_response(['code' => 1, 'msg' => '[步骤9]图生图失败:' . $e->getMessage(), 'data' => null]);
-        }
-    }
-
-    /**
-     * 将接口传入的图片路径转为本地绝对路径(与本地测试脚本逻辑一致)
-     * 支持:/uploads/xxx、uploads/xxx、public/uploads/xxx、或已是绝对路径
-     * @param string $path 接口传入的路径
-     * @return string 本地文件系统绝对路径
-     */
-    private function resolveImagePath($path)
-    {
-        $path = trim($path);
-        if (empty($path)) {
-            return '';
-        }
-        // 已是绝对路径且文件存在,直接返回(兼容本地测试传入的完整路径)
-        $pathNorm = str_replace('\\', '/', $path);
-        if (preg_match('#^[a-zA-Z]:/#', $pathNorm) || (strlen($pathNorm) > 1 && $pathNorm[0] === '/' && $pathNorm[1] !== '/')) {
-            if (file_exists($path)) {
-                return $pathNorm;
-            }
-        }
-        // 统一为相对于 public 的路径:uploads/xxx
-        $relPath = ltrim($pathNorm, '/');
-        if (strpos($relPath, 'public/') === 0) {
-            $relPath = substr($relPath, 7); // 去掉 public/
-        }
-        if (strpos($relPath, 'uploads/') !== 0) {
-            $relPath = 'uploads/' . ltrim($relPath, '/');
-        }
-        $fullPath = rtrim(str_replace('\\', '/', ROOT_PATH), '/') . '/public/' . $relPath;
-        return $fullPath;
-    }
-
-    /**
-     * 辅助函数:返回JSON响应
-     * @param array $data 响应数据
-     */
-    private function json_response($data)
-    {
-        header('Content-Type: application/json; charset=utf-8');
-        echo json_encode($data, JSON_UNESCAPED_UNICODE);
-        exit;
-    }
-
-    /**
-     * 辅助函数:图片URL/本地路径转Base64
-     * @param string $imgPath 图片URL/本地路径
-     * @return array 成功 ['mime','base64'] 失败 ['error'=>'错误信息']
-     */
-    private function img_to_base64($imgPath)
-    {
-        $imgContent = @file_get_contents($imgPath);
-        if (!$imgContent) {
-            return ['error' => '图片读取失败,请检查URL可访问性:' . $imgPath];
-        }
-        $finfo = new \finfo(FILEINFO_MIME_TYPE);
-        $mime = $finfo->buffer($imgContent);
-        if (!in_array($mime, ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'])) {
-            return ['error' => '不支持的图片格式:' . $mime . ',仅支持png/jpeg/webp'];
-        }
-        // 规范 MIME:Gemini 要求 image/jpeg 而非 image/jpg
-        if ($mime === 'image/jpg') {
-            $mime = 'image/jpeg';
-        }
-        // 去除 base64 中的空白字符,避免「格式错误」
-        $base64 = preg_replace('/\s+/', '', base64_encode($imgContent));
-        return ['mime' => $mime, 'base64' => $base64];
-    }
-
-
-    /**
-     * 图生图本地测试
-     */
-    public function imgtowimg()
-    {
-        $prompt = $this->request->param('prompt', '');
-        $imgRelPath = 'uploads/operate/ai/Preview/arr/0835006071623.png';
-
-        $imgPath = ROOT_PATH . 'public/' . $imgRelPath;
-
-        if (!file_exists($imgPath)) {
-            return json(['code' => 1, 'msg' => '原图不存在:' . $imgRelPath]);
-        }
-
-        $imgData = file_get_contents($imgPath);
-        $base64Img = 'data:image/png;base64,' . base64_encode($imgData);
-
-        $params = [
-            'prompt' => $prompt,
-            'negative_prompt' => '(deformed, distorted, disfigured:1.3), poorly drawn, bad anatomy',
-            'steps' => 20,
-            'sampler_name' => 'DPM++ 2M SDE',
-            'cfg_scale' => 7,
-            'seed' => -1,
-            'width' => 1024,
-            'height' => 1303,
-
-            'override_settings' => [
-                'sd_model_checkpoint' => 'realisticVisionV51_v51VAE-inpainting',
-                'sd_vae' => 'vae-ft-mse-840000-ema-pruned',
-                'CLIP_stop_at_last_layers' => 2
-            ],
-            'clip_skip' => 2,
-
-            'alwayson_scripts' => [
-                'controlnet' => [
-                    'args' => [[
-                        'enabled' => true,
-                        'input_image' => $base64Img,
-                        'module' => 'inpaint_only+lama',
-                        'model' => 'control_v11p_sd15_inpaint_fp16 [be8bc0ed]',
-                        'weight' => 1,
-                        'resize_mode' => 'Resize and Fill',
-                        'pixel_perfect' => false,
-                        'control_mode' => 'ControlNet is more important',
-                        'starting_control_step' => 0,
-                        'ending_control_step' => 1
-                    ]]
-                ]
-            ]
-        ];
-
-        $apiUrl = "http://20.0.17.188:45001/sdapi/v1/txt2img";
-        $headers = ['Content-Type: application/json'];
-
-        $ch = curl_init();
-        curl_setopt($ch, CURLOPT_URL, $apiUrl);
-        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-        curl_setopt($ch, CURLOPT_POST, true);
-        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
-        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params, JSON_UNESCAPED_UNICODE));
-        curl_setopt($ch, CURLOPT_TIMEOUT, 180);
-
-        $response = curl_exec($ch);
-        $error = curl_error($ch);
-        curl_close($ch);
-
-        if ($error) {
-            return json(['code' => 1, 'msg' => '请求失败:' . $error]);
-        }
-
-        $data = json_decode($response, true);
-        if (!isset($data['images'][0])) {
-            return json(['code' => 1, 'msg' => '接口未返回图像数据']);
-        }
-
-        $resultImg = base64_decode($data['images'][0]);
-        $saveDir = ROOT_PATH . 'public/uploads/img2img/';
-        if (!is_dir($saveDir)) {
-            mkdir($saveDir, 0755, true);
+        $result = $this->requestVectorEngine("/chat/completions", $data);
+        if ($result['code'] === 0) {
+            $this->success('识别成功', $result['data']);
+        } else {
+            $this->error($result['msg'], $result['data']);
         }
-
-        $originalBaseName = pathinfo($imgRelPath, PATHINFO_FILENAME);
-        $fileName = $originalBaseName . '-' . time() . '-1024x1248.png';
-        $savePath = $saveDir . $fileName;
-        file_put_contents($savePath, $resultImg);
-
-        return json([
-            'code' => 0,
-            'msg' => '图像生成成功',
-            'data' => [
-                'origin_url' => '/uploads/img2img/' . $fileName
-            ]
-        ]);
     }
 
     /**
-     * 后期图像处理-单张图片高清放大处理
+     * 文生视频接口
+     * POST /api/index/textToVideo
+     * 参数: prompt (string) 提示词, duration (int) 时长(秒), resolution (string) 分辨率
      */
-    public function extra_image()
+    public function textToVideo()
     {
-        // 配置参数
-        $config = [
-            'input_dir' => 'uploads/img2img/',
-            'output_dir' => 'uploads/extra_image/',
-            'api_url' => 'http://20.0.17.188:45001/sdapi/v1/extra-single-image',
-            'timeout' => 120, // 增加超时时间,高清处理可能耗时较长
-            'upscale_params' => [
-                'resize_mode' => 0,
-                'show_extras_results' => true,
-                'gfpgan_visibility' => 0, // 人脸修复关闭
-                'codeformer_visibility' => 0, // 人脸修复关闭
-                'codeformer_weight' => 0,
-                'upscaling_resize' => 2.45, // 放大倍数
-                'upscaling_crop' => true,
-                'upscaler_1' => 'R-ESRGAN 4x+ Anime6B', // 主放大模型
-                'upscaler_2' => 'None', // 不使用第二放大器
-                'extras_upscaler_2_visibility' => 0,
-                'upscale_first' => false,
-            ]
-        ];
-
-        // 输入文件处理
-        $imgRelPath = '0835006071623-1757406184-1024x1248.png';
-        $imgPath = ROOT_PATH . 'public/' . $config['input_dir'] . $imgRelPath;
-
-        if (!file_exists($imgPath)) {
-            return json(['code' => 1, 'msg' => '原图不存在:' . $imgRelPath]);
-        }
-
-        // 读取并编码图片
-        try {
-            $imgData = file_get_contents($imgPath);
-            if ($imgData === false) {
-                throw new Exception('无法读取图片文件');
-            }
-            $base64Img = base64_encode($imgData);
-        } catch (Exception $e) {
-            return json(['code' => 1, 'msg' => '图片处理失败:' . $e->getMessage()]);
-        }
-
-        // 准备API请求数据
-        $postData = array_merge($config['upscale_params'], ['image' => $base64Img]);
-        $jsonData = json_encode($postData);
-        if ($jsonData === false) {
-            return json(['code' => 1, 'msg' => 'JSON编码失败']);
-        }
-
-        // 调用API进行高清放大
-        $ch = curl_init();
-        curl_setopt_array($ch, [
-            CURLOPT_URL => $config['api_url'],
-            CURLOPT_RETURNTRANSFER => true,
-            CURLOPT_POST => true,
-            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
-            CURLOPT_POSTFIELDS => $jsonData,
-            CURLOPT_TIMEOUT => $config['timeout'],
-            CURLOPT_CONNECTTIMEOUT => 30,
-        ]);
-
-        $response = curl_exec($ch);
-        $error = curl_error($ch);
-        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-        curl_close($ch);
-
-        if ($error) {
-            return json(['code' => 1, 'msg' => 'API请求失败:' . $error]);
-        }
-
-        if ($httpCode !== 200) {
-            return json(['code' => 1, 'msg' => 'API返回错误状态码:' . $httpCode]);
-        }
-
-        $data = json_decode($response, true);
-        if (json_last_error() !== JSON_ERROR_NONE) {
-            return json(['code' => 1, 'msg' => 'API返回数据解析失败']);
-        }
+        $prompt     = $this->request->post('prompt');
+        $duration   = $this->request->post('duration', 5);
+        $resolution = $this->request->post('resolution', '720p');
 
-        if (!isset($data['image']) || empty($data['image'])) {
-            return json(['code' => 1, 'msg' => '接口未返回有效的图像数据']);
+        if (empty($prompt)) {
+            $this->error('提示词不能为空');
         }
 
-        // 保存处理后的图片
-        try {
-            $resultImg = base64_decode($data['image']);
-            if ($resultImg === false) {
-                throw new Exception('Base64解码失败');
-            }
-
-            $saveDir = ROOT_PATH . 'public/' . $config['output_dir'];
-            if (!is_dir($saveDir) && !mkdir($saveDir, 0755, true)) {
-                throw new Exception('无法创建输出目录');
-            }
-
-            $originalBaseName = pathinfo($imgRelPath, PATHINFO_FILENAME);
-            $fileName = $originalBaseName . '-hd.png'; // 使用-hd后缀更明确
-            $savePath = $saveDir . $fileName;
-
-            if (file_put_contents($savePath, $resultImg) === false) {
-                throw new Exception('无法保存处理后的图片');
-            }
-
-            // 返回成功响应
-            return json([
-                'code' => 0,
-                'msg' => '图像高清放大处理成功',
-                'data' => [
-                    'url' => '/' . $config['output_dir'] . $fileName,
-                    'original_size' => filesize($imgPath),
-                    'processed_size' => filesize($savePath),
-                    'resolution' => getimagesize($savePath), // 返回新图片的分辨率
-                ]
-            ]);
-
-        } catch (Exception $e) {
-            return json(['code' => 1, 'msg' => '保存结果失败:' . $e->getMessage()]);
-        }
-    }
-
-    /**
-     * 文本生成图片并保存第一张结果
-     * @param array $params 请求参数
-     * @return array 返回结果
-     */
-    public function txttowimg()
-    {
-        // API配置
-        $config = [
-            'api_url' => 'https://chatapi.onechats.ai/mj/submit/imagine',
-            'fetch_url' => 'https://chatapi.onechats.ai/mj/task/',
-            'api_key' => 'sk-iURfrAgzAjhZ4PpPLwzmWIAhM7zKfrkwDvyxk4RVBQ4ouJNK',
-            'default_prompt' => '一个猫',
-            'wait_time' => 3 // 等待生成完成的秒数
+        $data = [
+            "model"      => "kling-1.6", // 可替换为 seedance-2.0 等
+            "prompt"     => $prompt,
+            "duration"   => (int)$duration,
+            "resolution" => $resolution
         ];
-        try {
-            // 1. 准备请求数据
-            $prompt =  $config['default_prompt'];
-            $postData = [
-                'botType' => 'MID_JOURNEY',
-                'prompt' => $prompt,
-                'base64Array' => [],
-                'accountFilter' => [
-                    'channelId' => "",
-                    'instanceId' => "",
-                    'modes' => [],
-                    'remark' => "",
-                    'remix' => true,
-                    'remixAutoConsidered' => true
-                ],
-                'notifyHook' => "",
-                'state' => ""
-            ];
 
-            // 2. 提交生成请求
-            $generateResponse = $this->sendApiRequest($config['api_url'], $postData, $config['api_key']);
-            $generateData = json_decode($generateResponse, true);
-            if (empty($generateData['result'])) {
-                throw new Exception('生成失败: '.($generateData['message'] ?? '未知错误'));
-            }
-            $taskId = $generateData['result'];
-            // 3. 等待图片生成完成
-            sleep($config['wait_time']);
-            // 4. 获取生成结果
-            $fetchUrl = $config['fetch_url'].$taskId.'/fetch';
-            $fetchResponse = $this->sendApiRequest($fetchUrl, [], $config['api_key'], 'GET');
-            $fetchData = json_decode($fetchResponse, true);
-            if (empty($fetchData['imageUrl'])) {
-                throw new Exception('获取图片失败: '.($fetchData['message'] ?? '未知错误'));
-            }
-            // 5. 处理返回的图片数组(取第一张)
-            $imageUrls = is_array($fetchData['imageUrl']) ? $fetchData['imageUrl'] : [$fetchData['imageUrl']];
-            $firstImageUrl = $imageUrls[0];
-            // 6. 保存图片到本地
-            $savePath = $this->saveImage($firstImageUrl);
-            // 7. 返回结果
-            return [
-                'code' => 200,
-                'msg' => '图片生成并保存成功',
-                'data' => [
-                    'local_path' => $savePath,
-                    'web_url' => request()->domain().$savePath,
-                    'task_id' => $taskId
-                ]
-            ];
-        } catch (Exception $e) {
-            // 错误处理
-            return [
-                'code' => 500,
-                'msg' => '处理失败: '.$e->getMessage(),
-                'data' => null
-            ];
+        $result = $this->requestVectorEngine("/videos/generations", $data);
+        if ($result['code'] === 0) {
+            $this->success('生成成功', $result['data']);
+        } else {
+            $this->error($result['msg'], $result['data']);
         }
     }
 
     /**
-     * 发送API请求
-     * @param string $url 请求地址
-     * @param array $data 请求数据
-     * @param string $apiKey API密钥
-     * @param string $method 请求方法
-     * @return string 响应内容
-     * @throws Exception
+     * 封装向量引擎通用CURL请求
      */
-    private function sendApiRequest($url, $data, $apiKey, $method = 'POST')
+    private function requestVectorEngine($endpoint, $data)
     {
-        $ch = curl_init();
+        $url = $this->baseUrl . $endpoint;
+        $ch  = curl_init();
 
         curl_setopt_array($ch, [
-            CURLOPT_URL => $url,
+            CURLOPT_URL            => $url,
             CURLOPT_RETURNTRANSFER => true,
-            CURLOPT_CUSTOMREQUEST => $method,
-            CURLOPT_HTTPHEADER => [
-                'Authorization: Bearer '.$apiKey,
-                'Accept: application/json',
-                'Content-Type: application/json'
+            CURLOPT_POST           => true,
+            CURLOPT_POSTFIELDS     => json_encode($data),
+            CURLOPT_HTTPHEADER     => [
+                "Content-Type: application/json",
+                "Authorization: Bearer " . $this->apiKey
             ],
-            CURLOPT_POSTFIELDS => $method === 'POST' ? json_encode($data) : null,
-            CURLOPT_SSL_VERIFYPEER => false,
-            CURLOPT_SSL_VERIFYHOST => false,
-            CURLOPT_TIMEOUT => 60,
-            CURLOPT_FAILONERROR => true
+            CURLOPT_SSL_VERIFYPEER => false, // 生产环境建议开启
+            CURLOPT_TIMEOUT        => $this->timeout
         ]);
 
         $response = curl_exec($ch);
-        $error = curl_error($ch);
         $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        $error    = curl_error($ch);
         curl_close($ch);
 
         if ($error) {
-            throw new Exception('API请求失败: '.$error);
+            return ['code' => -1, 'msg' => '请求失败: ' . $error, 'data' => null];
         }
-
-        if ($httpCode < 200 || $httpCode >= 300) {
-            throw new Exception('API返回错误状态码: '.$httpCode);
-        }
-
-        return $response;
-    }
-
-    /**
-     * 保存图片到本地
-     * @param string $imageUrl 图片URL
-     * @return string 本地保存路径
-     * @throws Exception
-     */
-    private function saveImage($imageUrl)
-    {
-        // 1. 创建存储目录
-        $saveDir = ROOT_PATH.'public'.DS.'uploads'.DS.'midjourney'.DS.date('Ymd');
-        if (!is_dir($saveDir)) {
-            mkdir($saveDir, 0755, true);
-        }
-
-        // 2. 生成唯一文件名
-        $filename = uniqid().'.png';
-        $localPath = DS.'uploads'.DS.'midjourney'.DS.date('Ymd').DS.$filename;
-        $fullPath = $saveDir.DS.$filename;
-
-        // 3. 下载图片
-        $ch = curl_init($imageUrl);
-        curl_setopt_array($ch, [
-            CURLOPT_RETURNTRANSFER => true,
-            CURLOPT_FOLLOWLOCATION => true,
-            CURLOPT_SSL_VERIFYPEER => false,
-            CURLOPT_CONNECTTIMEOUT => 15
-        ]);
-
-        $imageData = curl_exec($ch);
-        $error = curl_error($ch);
-        curl_close($ch);
-
-        if (!$imageData) {
-            throw new Exception('图片下载失败: '.$error);
-        }
-
-        // 4. 验证图片类型
-        $imageInfo = getimagesizefromstring($imageData);
-        if (!in_array($imageInfo['mime'] ?? '', ['image/png', 'image/jpeg'])) {
-            throw new Exception('下载内容不是有效图片');
-        }
-
-        // 5. 保存文件
-        if (!file_put_contents($fullPath, $imageData)) {
-            throw new Exception('图片保存失败');
-        }
-
-        return $localPath;
-    }
-
-
-
-
-
-
-
-
-    /**
-     * 将图片背景变为透明
-     * @param string $imagePath 图片路径
-     * @return string 处理后的图片路径
-     */
-    public function makeImageBackgroundTransparent($imagePath) {
-        // 检查图片是否存在
-        if (!file_exists($imagePath)) {
-            return '图片不存在';
-        }
-
-        // 获取图片信息
-        $imageInfo = getimagesize($imagePath);
-        if (!$imageInfo) {
-            return '无法获取图片信息';
-        }
-        $width = $imageInfo[0];
-        $height = $imageInfo[1];
-
-        // 根据图片类型创建图像资源
-        switch ($imageInfo[2]) {
-            case IMAGETYPE_JPEG:
-                $source = imagecreatefromjpeg($imagePath);
-                break;
-            case IMAGETYPE_PNG:
-                $source = imagecreatefrompng($imagePath);
-                break;
-            case IMAGETYPE_GIF:
-                $source = imagecreatefromgif($imagePath);
-                break;
-            default:
-                return '不支持的图片类型';
-        }
-
-        if (!$source) {
-            return '无法创建图像资源';
-        }
-
-        // 创建透明画布
-        $transparent = imagecreatetruecolor($width, $height);
-        if (!$transparent) {
-            imagedestroy($source);
-            return '无法创建透明画布';
-        }
-        imagealphablending($transparent, false);
-        imagesavealpha($transparent, true);
-        $transparentColor = imagecolorallocatealpha($transparent, 255, 255, 255, 127);
-        imagefilledrectangle($transparent, 0, 0, $width, $height, $transparentColor);
-
-        // 背景色阈值(根据实际图片调整)
-        $bgThreshold = 150; // 降低阈值,提高背景检测灵敏度
-        $colorThreshold = 60; // 颜色差值阈值
-        $saturationThreshold = 20; // 饱和度阈值
-
-        // 逐像素处理
-        for ($x = 0; $x < $width; $x++) {
-            for ($y = 0; $y < $height; $y++) {
-                $pixelColor = imagecolorat($source, $x, $y);
-                $r = ($pixelColor >> 16) & 0xFF;
-                $g = ($pixelColor >> 8) & 0xFF;
-                $b = $pixelColor & 0xFF;
-
-                // 计算亮度
-                $brightness = ($r + $g + $b) / 3;
-
-                // 计算颜色饱和度
-                $max = max($r, $g, $b);
-                $min = min($r, $g, $b);
-                $saturation = ($max > 0) ? (($max - $min) / $max) * 100 : 0;
-
-                // 计算与背景色的差值(黑色/白色背景)
-                $bgDiff1 = sqrt(pow($r, 2) + pow($g, 2) + pow($b, 2)); // 与黑色的差值
-                $bgDiff2 = sqrt(pow(255 - $r, 2) + pow(255 - $g, 2) + pow(255 - $b, 2)); // 与白色的差值
-                $minBgDiff = min($bgDiff1, $bgDiff2);
-
-                // 综合判断:亮度较高(纸巾)或颜色与背景差异大的像素保留
-                if ($brightness > $bgThreshold || $minBgDiff > $colorThreshold || $saturation > $saturationThreshold) {
-                    // 保留前景像素
-                    imagesetpixel($transparent, $x, $y, $pixelColor);
-                }
-            }
-        }
-
-        // 生成输出路径
-        $saveDir = ROOT_PATH . 'public/uploads/ceshi/';
-        if (!is_dir($saveDir)) {
-            if (!mkdir($saveDir, 0755, true)) {
-                imagedestroy($source);
-                imagedestroy($transparent);
-                return '无法创建保存目录';
-            }
-        }
-
-        // 生成唯一文件名
-        $pathInfo = pathinfo($imagePath);
-        $filename = $pathInfo['filename'] . '_transparent_' . date('YmdHis') . '.png';
-        $outputPath = $saveDir . $filename;
-
-        // 保存透明图片
-        if (!imagepng($transparent, $outputPath)) {
-            imagedestroy($source);
-            imagedestroy($transparent);
-            return '无法保存透明图片';
+        if ($httpCode !== 200) {
+            return ['code' => $httpCode, 'msg' => 'API返回异常', 'data' => json_decode($response, true)];
         }
-
-        // 释放资源
-        imagedestroy($source);
-        imagedestroy($transparent);
-
-        return $outputPath;
+        return ['code' => 0, 'msg' => '成功', 'data' => json_decode($response, true)];
     }
 
-    /**
-     * 处理图片背景透明化请求
-     */
-    public function imgimg() {
-        // 使用本地图片路径
-        $localPath = ROOT_PATH . 'public/uploads/zhi.jpg';
-
-        // 检查图片是否存在
-        if (!file_exists($localPath)) {
-            return json(['code' => 1, 'msg' => '本地图片不存在,请检查文件路径']);
-        }
-
-        // 处理图片背景透明化
-        $result = $this->makeImageBackgroundTransparent($localPath);
-
-        // 返回结果
-        if (file_exists($result)) {
-            // 生成可访问的URL
-            $relativePath = str_replace(ROOT_PATH . 'public', '', $result);
-            // 确保路径以正斜杠开头
-            if (substr($relativePath, 0, 1) !== '/') {
-                $relativePath = '/' . $relativePath;
-            }
-            $imageUrl = 'http://20.0.16.128:9093' . $relativePath;
-
-            $res = [
-                'code' => 0,
-                'msg' => '处理成功',
-                'data' => [
-                    'transparent_image' => $imageUrl
-                ]
-            ];
-        } else {
-            $res = [
-                'code' => 1,
-                'msg' => $result
-            ];
-        }
-
-        return json($res);
-    }
 }

+ 351 - 10
application/api/controller/Material.php

@@ -16,21 +16,223 @@ class Material extends Api
     }
 
     /**
-     * 获取素材库列表接口
+     * 上传图片保存扩展名:优先客户端文件名,无后缀时用 MIME 推断,避免生成 xxx. 无扩展名
+     */
+    protected function resolveUploadedImageExt($file)
+    {
+        $name = $file->getInfo('name') ?? '';
+        $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
+        if ($ext === 'jpeg') {
+            $ext = 'jpg';
+        }
+        $allowed = ['jpg', 'png', 'gif', 'webp', 'bmp'];
+        if ($ext && in_array($ext, $allowed, true)) {
+            return $ext;
+        }
+        $mime = method_exists($file, 'getMime') ? strtolower((string) $file->getMime()) : '';
+        $map = [
+            'image/jpeg' => 'jpg',
+            'image/png' => 'png',
+            'image/gif' => 'gif',
+            'image/webp' => 'webp',
+            'image/bmp' => 'bmp',
+        ];
+        if (isset($map[$mime])) {
+            return $map[$mime];
+        }
+        return 'jpg';
+    }
+
+    /**
+     * 新增素材图片上传
+     *
+     * 方式一(推荐,与当前前端一致):multipart/form-data
+     *   - Category_id:分类 ID
+     *   - img[] 或 img:多张图片文件(字段名 img,多文件用 img[])
+     *   - material_name[] 或 material_name:与图片一一对应的名称数组
+     *   - sys_id:可选
+     *
+     * 方式二:JSON / 表单里传 uploaded_materials(base64)
+     *   - uploaded_materials=[{data:'data:image/png;base64,...', material_name:'xxx'}, ...]
+     */
+    public function Material_Add()
+    {
+        $params = $this->request->param();
+        $sysId = trim($params['sys_id'] ?? '');
+        $categoryId = isset($params['Category_id']) ? intval($params['Category_id']) : null;
+
+        $dateDir = date('Y-m-d');
+        $materialSavePath = str_replace('\\', '/', ROOT_PATH) . 'public/uploads/material/' . $dateDir . '/';
+        if (!is_dir($materialSavePath)) {
+            mkdir($materialSavePath, 0755, true);
+        }
+
+        $uploaded = [];
+
+        $files = $this->request->file('img');
+        if (!empty($files)) {
+            $fileList = is_array($files) ? $files : [$files];
+            $names = $params['material_name'] ?? [];
+            if (!is_array($names)) {
+                $names = [$names];
+            }
+            foreach ($fileList as $i => $file) {
+                if (!$file || !$file->isValid()) {
+                    continue;
+                }
+                $saveFileName = uniqid() . '_' . date('YmdHis') . '.' . $this->resolveUploadedImageExt($file);
+                $info = $file->move($materialSavePath, $saveFileName);
+                if (!$info) {
+                    continue;
+                }
+                $savedName = $info->getFilename();
+                $materialUrl = '/uploads/material/' . $dateDir . '/' . $savedName;
+                $materialRecord = [
+                    'sys_id'        => $sysId,
+                    'Category_id'   => $categoryId,
+                    'material_name' => trim($names[$i] ?? ''),
+                    'material_url'  => $materialUrl,
+                    'create_time'   => date('Y-m-d H:i:s'),
+                    'count'         => 1
+                ];
+                $materialId = Db::name('template_material')->insertGetId($materialRecord);
+                $uploaded[] = ['id' => $materialId, 'material_url' => $materialUrl];
+            }
+        } else {
+            $materials = $params['uploaded_materials'] ?? [];
+            if (empty($materials) || !is_array($materials)) {
+                return json(['code' => 1, 'msg' => '请上传至少一张素材图片(img/img[] 或 uploaded_materials)']);
+            }
+            foreach ($materials as $item) {
+                $base64Data = $item['data'] ?? '';
+                if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg|webp);base64,([^\)]+)/i', $base64Data, $m)) {
+                    continue;
+                }
+                $imageType = strtolower($m[1]);
+                $rawBase64 = preg_replace('/\s+/', '', $m[2]);
+                $imageData = base64_decode($rawBase64);
+                if ($imageData === false || strlen($imageData) < 100) {
+                    continue;
+                }
+                $ext = ($imageType === 'jpeg') ? 'jpg' : $imageType;
+                $fileName = uniqid() . '_' . date('YmdHis') . '.' . $ext;
+                $fullPath = $materialSavePath . $fileName;
+                if (!file_put_contents($fullPath, $imageData)) {
+                    continue;
+                }
+                $materialUrl = '/uploads/material/' . $dateDir . '/' . $fileName;
+                $materialRecord = [
+                    'sys_id'        => $sysId,
+                    'Category_id'   => $categoryId,
+                    'material_name' => trim($item['material_name'] ?? ''),
+                    'material_url'  => $materialUrl,
+                    'create_time'   => date('Y-m-d H:i:s'),
+                    'count'         => 1
+                ];
+                $materialId = Db::name('template_material')->insertGetId($materialRecord);
+                $uploaded[] = ['id' => $materialId, 'material_url' => $materialUrl];
+            }
+        }
+
+        if (empty($uploaded)) {
+            return json(['code' => 1, 'msg' => '没有有效的图片上传成功']);
+        }
+
+        return json(['code' => 0, 'msg' => '上传成功', 'data' => ['list' => $uploaded, 'count' => count($uploaded)]]);
+    }
+
+    /**
+     * 素材图片删除(软删除)
+     */
+    public function materialDelete()
+    {
+        $params = $this->request->param();
+        $record['mod_rq'] = date('Y-m-d H:i:s');
+        $res = Db::name('template_material')->where('id', $params['id'])->update($record);
+        if (!$res) {
+            return json([
+                'code' => 1,
+                'msg'  => '删除失败',
+                'data' => ''
+            ]);
+        }
+        return json([
+            'code' => 0,
+            'msg'  => '删除成功'
+        ]);
+    }
+
+    /**
+     * 素材修改
+     * 参数:id(必填), material_name(可选), Category_id(可选)
+     */
+    public function Material_Update()
+    {
+        $params = $this->request->param();
+        if (empty($params['id']) || !is_numeric($params['id'])) {
+            return json(['code' => 1, 'msg' => 'id 不能为空']);
+        }
+        $id = intval($params['id']);
+        $row = Db::name('template_material')->where('id', $id)->whereNull('mod_rq')->find();
+        if (!$row) {
+            return json(['code' => 1, 'msg' => '记录不存在或已删除']);
+        }
+
+        $update = ['update_time' => date('Y-m-d H:i:s')];
+        if (array_key_exists('material_name', $params)) {
+            $update['material_name'] = trim($params['material_name'] ?? '');
+        }
+        if (array_key_exists('Category_id', $params)) {
+            $update['Category_id'] = $params['Category_id'] === '' || $params['Category_id'] === null ? null : intval($params['Category_id']);
+        }
+
+        if (count($update) <= 1) {
+            return json(['code' => 1, 'msg' => '无有效修改字段']);
+        }
+
+        $affected = Db::name('template_material')->where('id', $id)->update($update);
+        if ($affected > 0) {
+            return json(['code' => 0, 'msg' => '修改成功']);
+        }
+        return json(['code' => 1, 'msg' => '修改失败']);
+    }
+
+    /**
+     * 获取素材库列表接口(分页+搜索)
+     * 参数:page(页码,从1开始), pageSize(每页条数,默认100,建议上限500)
+     * 搜索:对 material_name、category_name 模糊匹配
      */
     public function Material_List(){
         $params = $this->request->param();
-        $search = input('search', '');
+        $page = max(1, intval($params['page'] ?? 1));
+        $pageSize = min(500, max(1, intval($params['pageSize'] ?? 100)));
+
         $where = [];
-        if (!empty($search)) {
-            $where['type'] = ['like', '%' . $search . '%'];
+        if (!empty($params['search'])) {
+            // 使用更安全的查询方式,material_name 与 category_name 任一匹配即可
+            $search = trim($params['search']);
+            $where['a.material_name|b.category_name'] = ['like', '%' . $search . '%'];
         }
-        $res = Db::name('template_material')->where($where)->order('id desc')->select();
+        if (!empty($params['Category_id'])) {
+            $where['a.Category_id'] = ['like', '%' . $params['Category_id'] . '%'];
+        }
+
+        $query = Db::name('template_material')->alias('a')
+            ->field('a.id,a.sys_id,a.Category_id,b.category_name,a.material_name,a.material_url')
+            ->join('template_material_category b', 'a.Category_id = b.id AND b.mod_rq IS NULL', 'LEFT')
+            ->where($where)
+            ->whereNull('a.mod_rq')
+            ->order('a.id desc');
+
+        $total = (clone $query)->count();
+        $data = $query->page($page, $pageSize)->select();
+
         return json([
-            'code' => 0,
-            'msg'  => '',
-            'data' => $res,
-            'count' => count($res)
+            'code'  => 0,
+            'msg'   => '',
+            'data'  => $data,
+            'total' => $total,
+            'count' => count($data)
         ]);
     }
 
@@ -63,7 +265,7 @@ class Material extends Api
         }else{
             return json([
                 'code' => 1,
-                'msg'  => '此模版暂无作品',
+                'msg'  => '此模版作品暂无素材图',
                 'data' => '',
                 'count' => 0
             ]);
@@ -221,6 +423,7 @@ class Material extends Api
                     'fill_mode' => $layer['fill_mode'] ?? $layer['fillMode'] ?? '',//填充模式 solid/none
                     'fill_color' => $layer['fill_color'] ?? $layer['fillColor'] ?? '',//填充色
                     'stroke_color' => $layer['stroke_color'] ?? $layer['strokeColor'] ?? '',//描边色
+                    'page_index' => $layer['page_index'] ?? $layer['page_index'] ?? '',//画布分页排序
                     'stroke_width' => isset($layer['stroke_width']) ? floatval($layer['stroke_width']) : (isset($layer['strokeWidth']) ? floatval($layer['strokeWidth']) : 0),//描边宽度
                     'create_time' => date('Y-m-d H:i:s')
 
@@ -447,6 +650,7 @@ class Material extends Api
                     'fill_mode' => $layer['fill_mode'] ?? $layer['fillMode'] ?? '',//填充模式 solid/none
                     'fill_color' => $layer['fill_color'] ?? $layer['fillColor'] ?? '',//填充色
                     'stroke_color' => $layer['stroke_color'] ?? $layer['strokeColor'] ?? '',//描边色
+                    'page_index' => $layer['page_index'] ?? $layer['page_index'] ?? '',//画布分页排序
                     'stroke_width' => isset($layer['stroke_width']) ? floatval($layer['stroke_width']) : (isset($layer['strokeWidth']) ? floatval($layer['strokeWidth']) : 0),//描边宽度
                     'create_time' => date('Y-m-d H:i:s')
 
@@ -613,6 +817,143 @@ class Material extends Api
         ]);
     }
 
+    /**
+     * 素材分类查询(一级+二级树形结构,供前端展示)
+     */
+    public function Material_Category_List()
+    {
+        $all = Db::name('template_material_category')
+            ->field('id,category_name,parent_id,sort')
+            ->where('status', 1)
+            ->whereNull('mod_rq')
+            ->order('sort', 'asc')
+            ->select();
+
+        // 分离一级(parent_id=0)和二级
+        $level1 = [];
+        $level2ByParent = [];
+        foreach ($all as $row) {
+            if ($row['parent_id'] == 0) {
+                $row['children'] = [];
+                $level1[] = $row;
+            } else {
+                $pid = $row['parent_id'];
+                if (!isset($level2ByParent[$pid])) {
+                    $level2ByParent[$pid] = [];
+                }
+                $level2ByParent[$pid][] = $row;
+            }
+        }
+
+        // 按 sort 排序一级,并把二级挂到对应一级下
+        usort($level1, function ($a, $b) {
+            return (int)($a['sort'] ?? 0) - (int)($b['sort'] ?? 0);
+        });
+        foreach ($level1 as &$item) {
+            $item['children'] = $level2ByParent[$item['id']] ?? [];
+            usort($item['children'], function ($a, $b) {
+                return (int)($a['sort'] ?? 0) - (int)($b['sort'] ?? 0);
+            });
+        }
+
+        return json(['code' => 0, 'msg' => '', 'data' => $level1]);
+    }
+
+    /**
+     * 新增素材分类(支持一级、二级)
+     * 一级:parent_id 不传或传 0
+     * 二级:parent_id 传一级分类的 id
+     * 参数:category_name(必填), parent_id(可选,默认0), sort(可选,默认0), status(可选,默认1)
+     */
+    public function Material_Category_Add()
+    {
+        $params = $this->request->param();
+        if (empty(trim($params['category_name'] ?? ''))) {
+            return json(['code' => 1, 'msg' => '分类名称不能为空']);
+        }
+
+        $parentId = isset($params['parent_id']) ? intval($params['parent_id']) : 0;
+
+        // 二级分类:校验父分类存在且未软删除
+        if ($parentId > 0) {
+            $parent = Db::name('template_material_category')
+                ->where('id', $parentId)
+                ->where('parent_id', 0)
+                ->whereNull('mod_rq')
+                ->find();
+            if (!$parent) {
+                return json(['code' => 1, 'msg' => '父分类不存在或已删除']);
+            }
+        }
+
+        $data = [
+            'category_name' => trim($params['category_name']),
+            'parent_id' => $parentId,
+            'sort' => $params['sort'] ?? 0,
+            'status' => $params['status'] ?? '1',
+            'createtime' => date('Y-m-d H:i:s')
+        ];
+
+        $id = Db::name('template_material_category')->insertGetId($data);
+        if ($id) {
+            return json(['code' => 0, 'msg' => '新增成功', 'data' => ['id' => $id]]);
+        }
+        return json(['code' => 1, 'msg' => '新增失败']);
+    }
+
+    /**
+     * 修改素材分类
+     * 参数:id(必填), category_name/sort/status/parent_id(可选)
+     */
+    public function Material_Category_Update()
+    {
+        $params = $this->request->param();
+        if (empty($params['id']) || !is_numeric($params['id'])) {
+            return json(['code' => 1, 'msg' => '参数错误']);
+        }
 
+        $data = ['updatetime' => date('Y-m-d H:i:s')];
+        if (isset($params['category_name']) && trim($params['category_name']) !== '') {
+            $data['category_name'] = trim($params['category_name']);
+        }
+        if (isset($params['parent_id'])) {
+            $data['parent_id'] = intval($params['parent_id']);
+        }
+        if (isset($params['sort'])) {
+            $data['sort'] = $params['sort'];
+        }
+        if (isset($params['status'])) {
+            $data['status'] = $params['status'];
+        }
+
+        $affected = Db::name('template_material_category')
+            ->where('id', intval($params['id']))
+            ->whereNull('mod_rq')
+            ->update($data);
+
+        if ($affected > 0) {
+            return json(['code' => 0, 'msg' => '修改成功']);
+        }
+        return json(['code' => 1, 'msg' => '记录不存在或已删除']);
+    }
+
+    /**
+     * 删除素材分类(软删除,设置 mod_rq)
+     */
+    public function Material_Category_Delete()
+    {
+        $params = $this->request->param();
+        if (empty($params['id']) || !is_numeric($params['id'])) {
+            return json(['code' => 1, 'msg' => '参数错误']);
+        }
+
+        $affected = Db::name('template_material_category')
+            ->where('id', intval($params['id']))
+            ->update(['mod_rq' => date('Y-m-d H:i:s')]);
 
+        if ($affected > 0) {
+            return json(['code' => 0, 'msg' => '删除成功']);
+        }
+        return json(['code' => 1, 'msg' => '记录不存在或已删除']);
+    }
 }

+ 24 - 61
application/api/controller/Product.php

@@ -148,65 +148,6 @@ class Product extends Api
         $this->success('查询成功', $result);
     }
 
-    /**
-     * 产品原图图片上传
-     * @return \think\response\Json|void
-     */
-    public function ImgUpload()
-    {
-        // 处理 CORS OPTIONS 预检请求
-        if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
-            header('Access-Control-Allow-Origin: *');
-            header('Access-Control-Allow-Methods: POST, OPTIONS');
-            header('Access-Control-Allow-Headers: Content-Type, Authorization');
-            header('Access-Control-Max-Age: 86400');
-            exit(204);
-        }
-
-        // 实际请求必须返回 CORS 头
-        header('Access-Control-Allow-Origin: *');
-
-        // 获取上传的文件
-        $file = request()->file('image');
-        $param = $this->request->param();
-
-        if ($file) {
-            // 生成日期格式的文件夹名 image_YYYYMMDD
-            $dateFolder = 'image_' . date('Ymd');
-
-            // 指定目标目录(包含日期文件夹)
-            $targetPath = ROOT_PATH . 'public' . DS . 'uploads' . DS . 'merchant' . DS . $param['merchant_code'] . DS . $param['product_code'] . DS . 'oldimg' . DS . $dateFolder;
-
-            // 若目录不存在则创建
-            if (!is_dir($targetPath)) {
-                mkdir($targetPath, 0755, true);
-            }
-
-            // 获取原始文件名(或自定义新文件名)
-            $originalName = $file->getInfo('name'); // 原始文件名
-            $extension = pathinfo($originalName, PATHINFO_EXTENSION); // 文件扩展名
-            $newFileName = uniqid() . '.' . $extension; // 生成唯一文件名(避免冲突)
-
-            // 移动文件到指定目录,并验证大小/格式,同时指定自定义文件名
-            $info = $file->validate([
-                'size' => 10485760, // 最大10MB
-                'ext' => 'jpg,png'
-            ])->move($targetPath, $newFileName); // 关键:手动指定文件名,避免自动生成日期目录
-
-            if ($info) {
-                // 直接拼接路径,不依赖 getSaveName() 的返回值
-                $imageUrl = '/uploads/merchant/'.$param['merchant_code'].'/'.$param['product_code'].'/oldimg/' . $dateFolder . '/' . $newFileName;
-                return json(['code' => 0, 'msg' => '成功', 'data' => ['url' => $imageUrl]]);
-            } else {
-                $res = $file->getError();
-                return json(['code' => 1, 'msg' => '失败', 'data' => $res]);
-            }
-        }
-
-        return json(['code' => 1, 'msg' => '没有文件上传', 'data' => null]);
-    }
-
-
     /**
      * 产品详情
      * @return void
@@ -351,12 +292,12 @@ class Product extends Api
                     $safeName = preg_replace('/[\\\\\/:*?"<>|]/u', '_', $param['product_name']);
                     $ext = ($imageType === 'jpeg') ? 'jpg' : $imageType;
                     $fileName = $safeName . '.' . $ext;
-                    $saveDir = str_replace('\\', '/', ROOT_PATH . 'public/uploads/merchant/' . $prefix . '/' . $productCode . '/');
+                    $saveDir = str_replace('\\', '/', ROOT_PATH . 'public/uploads/merchant/' . $prefix . '/' . $productCode . '/'. 'oldimg/');
                     if (!is_dir($saveDir)) {
                         mkdir($saveDir, 0755, true);
                     }
                     if (file_put_contents($saveDir . $fileName, $imageData)) {
-                        $productImgPath = 'uploads/merchant/' . $prefix . '/' . $productCode . '/' . $fileName;
+                        $productImgPath = 'uploads/merchant/' . $prefix . '/' . $productCode . '/' . 'oldimg/'. $fileName;
                     }
                 }
             }
@@ -487,4 +428,26 @@ class Product extends Api
             $this->error('失败');
         }
     }
+
+    /**
+     * 产品删除(软删除,设置 deleteTime)
+     */
+    public function Product_Del()
+    {
+        $params = $this->request->param();
+        $record['deleteTime'] = date('Y-m-d H:i:s');
+        $res = Db::name('product')->where('id', $params['id'])->update($record);
+        if (!$res) {
+            return json([
+                'code' => 1,
+                'msg'  => '删除失败',
+                'data' => ''
+            ]);
+        }
+        return json([
+            'code' => 0,
+            'msg'  => '删除成功'
+        ]);
+    }
+
 }

+ 330 - 100
application/api/controller/WorkOrder.php

@@ -19,10 +19,34 @@ class WorkOrder extends Api{
     public function index(){echo '访问成功';}
 
     /**
-     * 获取图片生成状态
-     * @return json 任务状态和图片路径
+     * AI队列入口处理  出图接口
+     * 此方法处理图像转换为文本的请求,将图像信息存入队列以供后续处理。
+     */
+    public function imageToText()
+    {
+        $params = $this->request->param();
+        $service = new ImageService();
+        $service->handleImage($params);
+        $this->success('任务成功提交至队列');
+    }
+
+    /**
+     * task_id:查询获取任务图片
      */
     public function GetImageStatus(){
+        // $taskInfo = [
+        //     'status' => 'completed',
+        //     'image' => '/uploads/Product/img2img-20260317152818-69b902924548d.png',
+        //     'image_url' => '/uploads/Product/img2img-20260317152818-69b902924548d.png',
+        //     'completed_at' => date('Y-m-d H:i:s')
+        // ];
+        // $res = [
+        //     'code' => 0,
+        //     'msg' => '查询成功',
+        //     'data' => $taskInfo
+        // ];
+        // return json($res);die;
+
         $params = $this->request->param();
         $taskId = $params['task_id'];
         if (empty($taskId)) {
@@ -32,7 +56,7 @@ class WorkOrder extends Api{
             ];
             return json($res);
         }
-        // 从Redis中获取任务状态(支持文生图 text_to_image_task 和图生图 img_to_img_task)
+        //从Redis中获取任务状态
         $redis = getTaskRedis();
         $taskData = $redis->get("img_to_img_task:{$taskId}");
         if (!$taskData) {
@@ -55,98 +79,195 @@ class WorkOrder extends Api{
     }
 
     /**
-     * AI队列入口处理  出图接口
-     * 此方法处理图像转换为文本的请求,将图像信息存入队列以供后续处理。
+     * 支持的AI任务类型枚举(前端传入的status_val需在该列表内)
+     * 键:前端传入值,值:方法后缀( '前端传入值' => '方法后缀')
      */
-    public function imageToText()
+    private static $AI_TASK_TYPES = [
+        '图生文' => 'ImgToText',
+        '文生文' => 'TextToText',
+        '文生图' => 'TextToImg',
+        '图生图' => 'ImgToImg'
+    ];
+
+    /**
+     * AI模型接口统一调用入口
+     * @description 接收前端AI请求,校验任务类型合法性,分发至对应处理方法,统一异常捕获
+     * @return \think\response\Json 标准化JSON响应
+     */
+    public function callAIModelApi()
     {
-        $params = $this->request->param();
-        $service = new ImageService();
-        $service->handleImage($params);
-        $this->success('任务成功提交至队列');
+        try {
+            // 1. 获取并校验入参
+            $params = $this->request->param();
+//            echo "<pre>";
+//            print_r($params);
+//            echo "<pre>";die;
+            $statusVal = $this->validateAndGetStatusVal($params);
+
+            // 2. 映射并校验处理方法
+            $method = $this->getHandleMethod($statusVal);
+
+            // 3. 执行对应处理逻辑并返回响应
+            return $this->$method($params);
+        } catch (\InvalidArgumentException $e) {
+            // 参数/方法异常(用户侧错误)
+            return $this->jsonResponse(1, $e->getMessage());
+        } catch (\Throwable $e) {
+            // 系统异常(服务侧错误)
+            \think\Log::error('AI接口处理异常:' . $e->getMessage() . ' | 任务类型:' . ($params['status_val'] ?? '未知') . ' | 异常行:' . $e->getLine());
+            return $this->jsonResponse(1, '服务异常,请稍后重试');
+        }
     }
 
+    // -------------------------- 私有核心方法(通用逻辑) --------------------------
     /**
-     * product 产品专用统一api调用接口
-     * AI模型调用接口
+     * 校验并获取合法的任务类型
+     * @param array $params 前端入参
+     * @return string 合法的status_val
+     * @throws \InvalidArgumentException 任务类型不合法时抛出
      */
-    public function GetTxtToTxt(){
-        $params = $this->request->param();
-        $prompt = $params['prompt'];//提示词
-        $model = $params['model'];//模型
-        $old_path = $params['path'] ?? '';//原图路径
-        if(empty($model)) {
-            return json(['code' => 1, 'msg' => '模型请求失败']);
+    private function validateAndGetStatusVal(array $params): string
+    {
+        $statusVal = trim($params['status_val'] ?? '');
+        // 空值校验
+        if (empty($statusVal)) {
+            throw new \InvalidArgumentException('任务类型不能为空');
         }
+        // 合法性校验
+        if (!array_key_exists($statusVal, self::$AI_TASK_TYPES)) {
+            throw new \InvalidArgumentException('不支持的任务类型:' . $statusVal);
+        }
+        return $statusVal;
+    }
 
-        $aiGateway = new AIGatewayService();
-        $service = new ImageService();
+    /**
+     * 获取任务对应的处理方法名
+     * @param string $statusVal 合法的任务类型
+     * @return string 处理方法名(如handleAiImgToText)
+     * @throws \InvalidArgumentException 方法未实现时抛出
+     */
+    private function getHandleMethod(string $statusVal): string
+    {
+        $methodSuffix = self::$AI_TASK_TYPES[$statusVal];
+        $method = 'handleAi' . $methodSuffix;
+        if (!method_exists($this, $method)) {
+            throw new \InvalidArgumentException('任务类型暂未实现:' . $statusVal);
+        }
+        return $method;
+    }
 
-        //判断模型入口:status_val
-        if($params['status_val'] == '图生文'){
-            $service->handleImgToText($params);
-            $res = [
-                'code' => 0,
-                'msg' => '正在优化提示词,请稍等.....'
-            ];
-            return json($res);
-        }else if($params['status_val'] == '文生文'){
-            $fullPrompt = $prompt;
-            $fullPrompt .= "
-                        请根据上述内容生成一段完整的话术,要求:
-                        1. 内容必须是连贯的一段话,不要使用列表、分隔线或其他结构化格式
-                        2. 不要包含非文本元素的描述
-                        3. 不要添加任何额外的引导语、解释或开场白
-                        4. 语言流畅自然";
-            $result = $service->handleTextToText($fullPrompt, $model);
-
-            if(!empty($params['id'])){
-                Db::name('product')->where('id',  $params['id'])->update(['content' => $result['data']]);
-            }
-            $res = [
-                'code' => 0,
-                'msg' => '优化成功',
-                'content' => $result['data']
-            ];
-            return json($res);
+    /**
+     * 通用JSON响应封装
+     * @param int $code 响应码(0=成功,1=失败)
+     * @param string $msg 响应信息
+     * @param array $data 响应数据(可选)
+     * @return \think\response\Json
+     */
+    private function jsonResponse(int $code, string $msg, array $data = []): \think\response\Json
+    {
+        $response = [
+            'code' => $code,
+            'msg' => $msg,
+            'time' => date('Y-m-d H:i:s')
+        ];
 
-        }elseif($params['status_val'] == '文生图'){
-            $result = $service->handleTextToImg($params);
-            if ($result['success']) {
-                $res = [
-                    'code' => 0,
-                    'msg' => $result['message'],
-                    'data' => [
-                        'task_id' => $result['task_id']
-                    ]
-                ];
-            } else {
-                $res = [
-                    'code' => 1,
-                    'msg' => $result['message']
-                ];
-            }
-            return json($res);
-        }elseif($params['status_val'] == '图生图'){
-            $result = $service->handleImgToImg($params);
-            if (isset($result['success']) && $result['success']) {
-                return json([
-                    'code' => 0,
-                    'time' => date('Y-m-d H:i:s'),
-                    'msg' => $result['message'],
-                    'data' => ['task_id' => $result['task_id'] ?? '']
-                ]);
-            }
-            return json([
-                'code' => 1,
-                'msg' => $result['message'] ?? '图生图任务提交失败',
-                'data' => null
-            ]);
-        }else{
-            $this->error('请求失败');
+        if (!empty($data)) {
+            $response['data'] = $data;
         }
+
+        return json($response);
+    }
+
+    /**
+     * 任务类接口统一响应(带task_id的场景)
+     * @param array $result 业务处理结果(需包含success字段)
+     * @param string $failMsg 失败提示语
+     * @return \think\response\Json
+     */
+    private function jsonTaskResponse(array $result, string $failMsg = '任务提交失败'): \think\response\Json
+    {
+        $isSuccess = isset($result['success']) && $result['success'];
+
+        $data = $isSuccess ? ['task_id' => $result['task_id'] ?? ''] : [];
+        $msg = $isSuccess ? ($result['message'] ?? '提交成功') : ($result['message'] ?? $failMsg);
+
+        return $this->jsonResponse($isSuccess ? 0 : 1, $msg, $data);
+    }
+
+    // -------------------------- 业务处理方法(按任务类型拆分) --------------------------
+    /**
+     * 图生文任务处理:提交队列并返回提示
+     * @param array $params 前端入参
+     * @return \think\response\Json
+     */
+    private function handleAiImgToText(array $params): \think\response\Json
+    {
+        (new ImageService())->handleImgToText($params);
+        return $this->jsonResponse(0, '正在优化提示词,请稍等.....');
     }
 
+    /**
+     * 文生文任务处理:生成话术并返回内容(支持产品内容更新)
+     * @param array $params 前端入参
+     * @return \think\response\Json
+     */
+    private function handleAiTextToText(array $params): \think\response\Json
+    {
+//        echo "<pre>";
+//        print_r($params);
+//        echo "<pre>";die;
+        //构造生成提示词
+        $promptTemplate = "\n请根据上述内容生成一段完整的话术,要求:\n"
+            . "1. 内容必须是连贯的一段话,不要使用列表、分隔线或其他结构化格式\n"
+            . "2. 不要包含非文本元素的描述\n"
+            . "3. 不要添加任何额外的引导语、解释或开场白\n"
+            . "4. 禁忌:不添加无关形容词,不修改产品核心信息,语言流畅自然";
+        $prompt = ($params['prompt'] ?? '') . $promptTemplate;
+        // 调用服务层生成内容
+        $result = (new ImageService())->handleTextToText(
+            $params['status_val'],
+            $prompt,
+            $params['model']
+        );
+        if (empty($result['success'])) {
+            return $this->jsonResponse(1, $result['message'] ?? '生成失败');
+        }
+        $content = $result['data'] ?? '';
+
+        //区分业务场景处理
+        $isProductImageGeneration = ($params['status_type'] ?? '') === 'ProductImageGeneration';
+        $isProductTemplateReplace = ($params['status_type'] ?? '') === 'ProductTemplateReplace';
+
+        if (!$isProductImageGeneration && !$isProductTemplateReplace) {
+            Db::name('product')->where('id', $params['id'])->update(['content' => $content]);
+        }
+
+        return $this->jsonResponse(0, '优化成功', ['content' => $content]);
+    }
+
+    /**
+     * 文生图任务处理:提交任务并返回task_id
+     * @param array $params 前端入参
+     * @return \think\response\Json
+     */
+    private function handleAiTextToImg(array $params): \think\response\Json
+    {
+        $serviceResult = (new ImageService())->handleTextToImg($params);
+        return $this->jsonTaskResponse($serviceResult, '文生图任务提交失败');
+    }
+
+    /**
+     * 图生图任务处理:提交任务并返回task_id
+     * @param array $params 前端入参
+     * @return \think\response\Json
+     */
+    private function handleAiImgToImg(array $params): \think\response\Json
+    {
+        $serviceResult = (new ImageService())->handleImgToImg($params);
+        return $this->jsonTaskResponse($serviceResult, '图生图任务提交失败');
+    }
+
+
     /**
      * 即梦AI--创建视频任务接口
      * 首帧图 + 尾帧图 = 新效果视频
@@ -391,7 +512,7 @@ class WorkOrder extends Api{
         // 发起接口请求
 //        $apiUrl = 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:generateContent';
         $apiUrl = 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:streamGenerateContent';
-        $apiKey = 'sk-9aIV9nx7pJxJFMrB8REtNbhjYuNBxCcnEOwiJDHd6UwmN2eJ';
+        $apiKey = '';
 
 //        $params = $this->request->param();
         $prompt = '生成一个苹果(九个分镜头图片)';
@@ -881,7 +1002,9 @@ class WorkOrder extends Api{
         curl_close($ch);
 
         $responseData = json_decode($body, true);
-
+        echo "<pre>";
+        print_r($responseData);
+        echo "<pre>";die;
         // 检查API是否返回了错误信息
         if (isset($responseData['error'])) {
             $errorMessage = isset($responseData['error']['message']) ? $responseData['error']['message'] : 'API请求失败';
@@ -928,7 +1051,9 @@ class WorkOrder extends Api{
             'size' => $postData['size'],
             'sys_rq' => date("Y-m-d H:i:s")
         ];
-
+        echo "<pre>";
+        print_r($videoData);
+        echo "<pre>";die;
         // 尝试插入数据
         try {
             $res = Db::name('video')->insert($videoData);
@@ -959,7 +1084,7 @@ class WorkOrder extends Api{
         // 从请求参数获取video_id,如果没有则使用默认值
         $video_id = input('get.video_id');
 
-        $apiKey = 'sk-sWW1GFlnjbrDRb1DkMEzePIxgdvLK6cZt0Qg93yDMVP2z1yN';
+        $apiKey = '';
         // 1. 先检查视频状态
         $statusUrl = 'https://chatapi.onechats.ai/v1/videos/' . $video_id;
         $statusData = $this->fetchVideoStatus($statusUrl, $apiKey);
@@ -1376,6 +1501,7 @@ class WorkOrder extends Api{
 
     /**
      * 新增模版
+     * (暂时无用)
      */
     public function Add_Product_Template(){
         try {
@@ -1409,9 +1535,6 @@ class WorkOrder extends Api{
                 'sys_rq' => date('Y-m-d'),
                 'create_time' => date('Y-m-d H:i:s')
             ];
-//            echo "<pre>";
-//            print_r($data);
-//            echo "<pre>";die;
             $result = Db::name('product_template')->insert($data);
             if ($result) {
                 return json(['code' => 0, 'msg' => '模板保存成功']);
@@ -1447,17 +1570,6 @@ class WorkOrder extends Api{
                 ->whereNull('mod_rq')
                 ->select();
         }
-
-//        $http_url = Db::name('http_url')->field('baseUrl,port')->find();
-//        if ($products && $http_url) {
-//            $base_url = !empty($http_url['baseUrl']) && !empty($http_url['port'])
-//                ? 'http://' . $http_url['baseUrl'] . ':' . $http_url['port'] : '';
-//            if ($base_url) {
-//                foreach ($products as &$val) {
-//                    $val['template_image_url'] = $base_url . $val['template_image_url'];
-//                }
-//            }
-//        }
         return json([
             'code' => 0,
             'msg' => '请求成功',
@@ -1484,4 +1596,122 @@ class WorkOrder extends Api{
         ];
         return json($res);
     }
+
+    /**
+     * 获取 AI 模型配置
+     * status 1 = 启用 0 = 禁用(同一模型内的优先级,数值越小越优先)
+     * model_type 支持多能力逗号间隔(如 文生图,图生图),传参精确匹配某一能力
+     * 可选参数:manage=1 时返回全部(含禁用),用于管理端
+     */
+    public function GetAIModel(){
+        $params = $this->request->param();
+        $query = Db::name('ai_model');
+        if (empty($params['manage'])) {
+            $query->where('status', '1');
+        }
+        if (!empty($params['model_type'])) {
+            $query->whereRaw('FIND_IN_SET(:mt, model_type) > 0', ['mt' => trim($params['model_type'])]);
+        }
+        if (!empty($params['supplier'])) {
+            $query->where('supplier', 'like', '%' . $params['supplier'] . '%');
+        }
+        if (!empty($params['model_name'])) {
+            $query->where('model_name|model_alias', 'like', '%' . $params['model_name'] . '%');
+        }
+        $data = $query->order('sort ASC, id ASC')->select();
+        return json([
+            'code' => 0,
+            'msg' => '成功',
+            'data' => $data
+        ]);
+    }
+
+    /**
+     * 新增 AI 模型配置
+     * POST: status, supplier, api_key, api_url, model_group, model_name, model_alias, model_type, sort
+     * model_type 多能力用逗号间隔,如:文生图,图生图
+     */
+    public function AddAIModel(){
+        $params = $this->request->param();
+        $required = ['api_url', 'api_key', 'model_name', 'model_type'];
+        foreach ($required as $k) {
+            if (empty(trim($params[$k] ?? ''))) {
+                return json(['code' => 1, 'msg' => $k . ' 不能为空']);
+            }
+        }
+        $insert = [
+            'status'      => isset($params['status']) ? trim($params['status']) : '1',
+            'supplier'    => trim($params['supplier'] ?? ''),
+            'api_key'     => trim($params['api_key']),
+            'api_url'     => trim($params['api_url']),
+            'model_group' => trim($params['model_group'] ?? ''),
+            'model_name'  => trim($params['model_name']),
+            'model_alias' => trim($params['model_alias'] ?? $params['model_name'] ?? ''),
+            'model_type'  => trim($params['model_type']),
+            'sort'        => isset($params['sort']) ? intval($params['sort']) : 0,
+        ];
+        try {
+            Db::name('ai_model')->insert($insert);
+            return json(['code' => 0, 'msg' => '新增成功']);
+        } catch (\Exception $e) {
+            return json(['code' => 1, 'msg' => '新增失败: ' . $e->getMessage()]);
+        }
+    }
+
+    /**
+     * 修改 AI 模型配置
+     * POST: id(必填), 其余字段同新增
+     */
+    public function UpdateAIModel(){
+        $params = $this->request->param();
+        if (empty($params['id'])) {
+            return json(['code' => 1, 'msg' => 'id 不能为空']);
+        }
+        $id = intval($params['id']);
+        $exists = Db::name('ai_model')->where('id', $id)->find();
+        if (!$exists) {
+            return json(['code' => 1, 'msg' => '记录不存在']);
+        }
+        $update = [];
+        $fields = ['status', 'supplier', 'api_key', 'api_url', 'model_group', 'model_name', 'model_alias', 'model_type', 'sort'];
+        foreach ($fields as $f) {
+            if (array_key_exists($f, $params)) {
+                $update[$f] = $f === 'sort' ? intval($params[$f]) : trim($params[$f] ?? '');
+            }
+        }
+        if (empty($update)) {
+            return json(['code' => 1, 'msg' => '无有效修改字段']);
+        }
+        try {
+            Db::name('ai_model')->where('id', $id)->update($update);
+            return json(['code' => 0, 'msg' => '修改成功']);
+        } catch (\Exception $e) {
+            return json(['code' => 1, 'msg' => '修改失败: ' . $e->getMessage()]);
+        }
+    }
+
+    /**
+     * 用于获取所有产品记录
+     **/
+    public function GetProductList(){
+        $params = $this->request->param();
+        // 构建查询条件
+        $where = [];
+        if (!empty($params['search'])) {
+            $where['prompt|model'] = ['like', '%' . $params['search'] . '%'];
+        }
+        if (!empty($params['sys_id'])) {
+            $where['sys_id'] = ['like', '%' . $params['sys_id'] . '%'];
+        }
+        $data = Db::name('product_image_generate')->where($where)->order('id desc')->limit(0,10)->select();
+
+        $prompt = Db::name('product_image_generate')->field('prompt')->where($where)->group('prompt')->order('id desc')->select();
+
+        return json([
+            'code' => 0,
+            'msg' => '成功',
+            'prompt' => $prompt,
+            'data' => $data
+        ]);
+    }
 }

+ 1 - 20
application/database.php

@@ -56,24 +56,5 @@ return [
     // 时间字段取出后的默认时间格式,默认为Y-m-d H:i:s
     'datetime_format' => false,
     // 是否需要进行SQL性能分析
-    'sql_explain'     => false,
-    // MongoDB数据库配置
-    'mongodb' => [
-        // type 和 query 必须写成这个类名(如果你用的是 think-mongo 1.1)
-        'type'        => '\think\mongo\Connection',
-        'query'       => '\think\mongo\Query',
-        // MongoDB 服务器信息
-        'hostname'    => '20.0.16.79',   // 改成你的MongoDB服务器IP
-        'hostport'    => 27017,          // 默认端口
-        // 数据库名(⚠️ 必须填写)
-        'database'    => 'qiqi',
-        // 账号密码(如果没有就留空)
-        'username'    => '',
-        'password'    => '',
-        // 认证源(如果没启用认证就可以留空或删除这行)
-        'params'      => [],
-        // 其他配置
-        'pk_convert_id' => false,  // 推荐false,防止ObjectId自动转字符串
-        'debug'          => true,  // 开启调试
-    ],
+    'sql_explain'     => false
 ];

+ 9 - 4
application/job/ImageArrJob.php

@@ -24,20 +24,25 @@ class ImageArrJob
         // 将 $data 强制转换为数组(兼容对象/字符串等类型)
         $data = (array)$data;
 
-        if (empty($data['status_val']) || $data['status_val'] == '文生图') {
+        if (empty($data['status_val']) || $data['status_val'] == '文生文') {
+
+            Queue::push('app\job\TextToTextJob', $data, 'txttotxt');
+            echo date('Y-m-d H:i:s') . " 文生文已启动\n";
+
+        }elseif (empty($data['status_val']) || $data['status_val'] == '文生图') {
 
             Queue::push('app\job\TextToImageJob', $data, 'txttoimg');
-            echo date('Y-m-d H:i:s') . " 文生图队列已启动\n";
+            echo date('Y-m-d H:i:s') . " 文生图已启动\n";
 
         }else if (empty($data['status_val']) || $data['status_val'] == '图生文') {
 
             Queue::push('app\job\ImageJob', $data, 'imgtotxt');
-            echo date('Y-m-d H:i:s') . " 图生文队列已启动\n";
+            echo date('Y-m-d H:i:s') . " 图生文已启动\n";
 
         }else if (empty($data['status_val']) || $data['status_val'] == '图生图') {
 
             Queue::push('app\job\ImageToImageJob', $data, 'imgtoimg');
-            echo date('Y-m-d H:i:s') . " 图生图队列已启动\n";
+            echo date('Y-m-d H:i:s') . " 图生图已启动\n";
 
         } else {
 

+ 5 - 7
application/job/ImageJob.php

@@ -145,10 +145,8 @@ class ImageJob{
         $prompt = $params['prompt'];//提示词
         $old_path = $params['path'] ?? '';//原图路径
         $model = $params['model'];//模型
+        $status_val = $params['status_val'];
 
-        if(empty($model)) {
-            return json(['code' => 1, 'msg' => '模型请求失败']);
-        }
         $aiGateway = new AIGatewayService();
         // 获取图片的base64数据和MIME类型
         $imageData = AIGatewayService::file_get_contents($old_path);
@@ -167,14 +165,14 @@ class ImageJob{
             4. 输出格式:不允许添加任何解释、引导、说明、示例、备注,仅返回「产品描述 + 替换后提示词」,直接输出纯文本;
             【模板提示词】{$prompt}";
 
-        $result = $aiGateway->callGptApi($model, $formattedPrompt, $mimeType, $base64Data);
+        $result = $aiGateway->buildRequestData($status_val,$model, $formattedPrompt, $base64Data,$mimeType);
 
         // Gemini模型响应格式处理
         $imgToTxtContent = $result['candidates'][0]['content']['parts'][0]['text'];
 
         // // 图生文成功后,调用文生文功能(gemini-2.0-flash)
         $txtToTxtPrompt = "转换成中文格式,去掉其他特殊符号,不允许添加任何解释、引导、说明、示例等文字:\n\n{$imgToTxtContent}";
-        $txtToTxtResult = $aiGateway->txtGptApi($txtToTxtPrompt, 'gemini-2.0-flash');
+        $txtToTxtResult = $aiGateway->buildRequestData($status_val,$model,$txtToTxtPrompt);
         $finalContent = $txtToTxtResult['candidates'][0]['content']['parts'][0]['text'];
 
         // 处理调试输出内容
@@ -236,10 +234,10 @@ class ImageJob{
 
         // 调用 图生文 接口
         $ai = new AIGatewayService();
-        $gptRes = $ai->callGptApi($imgtotxt_selectedOption,$prompt,$imageUrl,'');
+        $gptRes = $ai->Img_To_TxtCallApi($imgtotxt_selectedOption,$prompt,$imageUrl,'');
 
         $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
-    
+
         //图生文返回内容日志runtime/logs/img_to_txt.txt
         // file_put_contents(
         //     $logDir . 'img_to_txt.txt',

+ 177 - 73
application/job/ImageToImageJob.php

@@ -9,8 +9,8 @@ class ImageToImageJob{
     public function fire(Job $job, $data)
     {
         //产品图+模板图) 时走此分支
-        if (isset($data['status_val']) && $data['status_val'] == '图生图' && !empty($data['product_img']) && !empty($data['template_img'])) {
-            $taskId = $data['task_id'] ?? '';
+        if (isset($data['status_val']) && $data['status_val'] == '图生图') {
+            $taskId = $data['task_id'];
             // 幂等:若任务已完成则跳过,避免超时重试导致重复执行
             $redis = getTaskRedis();
             $existing = $redis->get("img_to_img_task:{$taskId}");
@@ -23,12 +23,7 @@ class ImageToImageJob{
                 }
             }
             try {
-
-                // 获取产品ID
-                $Id = $data['id'];
-
-                echo " 图生图任务开始处理\n";
-                echo "开始时间:" . date('Y-m-d H:i:s') . "\n";
+                echo " 开始处理图生图".date('Y-m-d H:i:s')."\n";
 
                 $result = $this->get_img_to_img($data);
                 // get_img_to_img 内部已写入 img_to_img_task,此处无需重复写入
@@ -170,10 +165,64 @@ class ImageToImageJob{
     {
 
         $prompt = trim($data['prompt'] ?? '');
-        $size = trim($data['size'] ?? $data['$size'] ?? '');
+        $size = trim($data['size'] ?? '');
+        $status_val = trim($data['status_val'] ?? '');
         $product_img = trim($data['product_img'] ?? '');
         $template_img = trim($data['template_img'] ?? '');
-        $model = trim($data['model'] ?? 'gemini-3-pro-image-preview');
+        $model = trim($data['model']);
+        $sys_id = trim($data['sys_id']);
+        $date = date('Y-m-d H:i:s');
+
+        $setTaskError = function ($msg) use ($data) {
+            if (!empty($data['task_id'])) {
+                try {
+                    $redis = getTaskRedis();
+                    $redis->set("img_to_img_task:" . $data['task_id'], json_encode([
+                        'status' => 'failed',
+                        'msg' => $msg,
+                        'error' => $msg,
+                        'completed_at' => date('Y-m-d H:i:s')
+                    ], JSON_UNESCAPED_UNICODE), ['EX' => 300]);
+                } catch (\Exception $e) {
+                }
+            }
+        };
+
+        $product_base64Data = null;
+        $product_mimeType = 'image/png';
+        $template_base64Data = null;
+        $template_mimeType = 'image/png';
+        if ($data['status_type'] == 'ProductImageGeneration') {
+            //产品图创作页面参数配置
+            // 前端传 base64,先解析用于调接口,成功后再存文件与数据库
+            preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $product_img, $pm);
+            if (empty($pm)) {
+                $setTaskError('产品图未找到图片数据');
+                return ['code' => 1, 'msg' => '产品图未找到图片数据'];
+            }
+            $product_base64Data = preg_replace('/\s+/', '', $pm[2]);
+            $product_mimeType = ($pm[1] == 'jpg' ? 'image/jpeg' : 'image/' . $pm[1]);
+            $product_img_ext = $pm[1];
+
+            preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $template_img, $tm);
+            if (empty($tm)) {
+                $setTaskError('模板图未找到图片数据');
+                return ['code' => 1, 'msg' => '模板图未找到图片数据'];
+            }
+            $template_base64Data = preg_replace('/\s+/', '', $tm[2]);
+            $template_mimeType = ($tm[1] == 'jpg' ? 'image/jpeg' : 'image/' . $tm[1]);
+            $template_img_ext = $tm[1];
+        } else if($data['status_type'] == 'ProductTemplateReplace'){
+            //产品替换页面参数配置
+            $productImgRaw = AIGatewayService::file_get_contents($product_img);
+            $product_base64Data = $productImgRaw['base64Data'];
+            $product_mimeType = $productImgRaw['mimeType'];
+            $templateImgRaw = AIGatewayService::file_get_contents($template_img);
+            $template_base64Data = $templateImgRaw['base64Data'];
+            $template_mimeType = $templateImgRaw['mimeType'];
+        }else {
+            return ['code' => 1, 'msg' => '当前页面未进行配置,请联系管理员开通权限'];
+        }
 
         $defaultPrompt = '请完成产品模板替换:
                             1. 从产品图提取产品主体、品牌名称、核心文案;
@@ -181,28 +230,9 @@ class ImageToImageJob{
                             3. 将模板图中的产品和文字替换为产品图的内容;
                             4. 最终生成的图片与模板图视觉风格100%统一,仅替换产品和文字。';
         $promptContent = $prompt ? $prompt . "\n\n" . $defaultPrompt : $defaultPrompt;
-
         $aiGateway = new AIGatewayService();
+        $res = $aiGateway->buildRequestData($status_val,$model,$promptContent,$size,$product_base64Data,$product_mimeType,$template_base64Data,$template_mimeType);
 
-        // 获取图片的base64数据和MIME类型
-        $productImgRaw = AIGatewayService::file_get_contents($product_img);
-        $product_base64Data = $productImgRaw['base64Data'];
-        $product_mimeType = $productImgRaw['mimeType'];
-        $templateImgRaw = AIGatewayService::file_get_contents($template_img);
-        $template_base64Data = $templateImgRaw['base64Data'];
-        $template_mimeType = $templateImgRaw['mimeType'];
-
-        $res = $aiGateway->GeminiImToImgCallApi($promptContent, $model,$size, $product_base64Data,$product_mimeType,$template_base64Data,$template_mimeType);
-//        echo "<pre>";
-//        print_r(113322);
-//        echo "<pre>";
-//        echo "<pre>";
-//        print_r($res);
-//        echo "<pre>";die;
-//        if (isset($res['code']) && $res['code'] !== 0) {
-//            return ['code' => 1, 'msg' => $res['msg'] ?? '图生图失败'];
-//        }
-        // API 可能返回 inlineData.data 或 text 两种都支持
         $base64Data = null;
         if (isset($res['candidates'][0]['content']['parts'][0]['inlineData']['data'])) {
             $base64Data = $res['candidates'][0]['content']['parts'][0]['inlineData']['data'];
@@ -215,61 +245,136 @@ class ImageToImageJob{
         }
         if (!$base64Data) {
             $errMsg = isset($res['error']['message']) ? $res['error']['message'] : '未获取到图片数据';
+            $setTaskError($errMsg);
             return ['code' => 1, 'msg' => $errMsg];
         }
         $imageData = base64_decode($base64Data);
 
         if ($imageData === false || strlen($imageData) < 100) {
+            $setTaskError('图片Base64解码失败');
             return ['code' => 1, 'msg' => '图片Base64解码失败'];
         }
 
-        $rootPath = str_replace('\\', '/', ROOT_PATH);
-        $saveDir = rtrim($rootPath, '/') . '/public/uploads/template/';
+        if ($data['status_type'] == 'ProductImageGeneration') {
+            // 接口成功后再存文件与数据库
+            $rootPath = str_replace('\\', '/', ROOT_PATH);
+            $saveDir = rtrim($rootPath, '/') . '/public/uploads/Product/' . date('Y-m-d') . '/';
+            if (!is_dir($saveDir)) {
+                mkdir($saveDir, 0755, true);
+            }
 
-        if (!is_dir($saveDir)) {
-            mkdir($saveDir, 0755, true);
-        }
-        $fileName = 'img2img-' . date('YmdHis') . '-' . uniqid() . '.png';
-        $fullPath = $saveDir . $fileName;
-        if (!file_put_contents($fullPath, $imageData)) {
-            return ['code' => 1, 'msg' => '图片保存失败'];
-        }
+            $product_file = 'product-' . uniqid() . '.' . $product_img_ext;
+            $product_image_data = base64_decode($product_base64Data);
+            if ($product_image_data === false || !file_put_contents($saveDir . $product_file, $product_image_data)) {
+                $setTaskError('产品图保存失败');
+                return ['code' => 1, 'msg' => '产品图保存失败'];
+            }
+            $product_db_path = '/uploads/Product/' . date('Y-m-d') . '/' . $product_file;
 
-        $db_img_path = '/uploads/template/' . $fileName;
-
-        Db::name('product')->where('id',  $data['id'])->update
-        (
-            [
-                'createTime' => date('Y-m-d H:i:s'),
-                'content' => $data['prompt'],
-                'product_new_img' => $db_img_path
-            ]
-        );
-        //生成新图后保存到记录 存留历史图片
-        $record['product_id'] = $data['id'];
-        $record['product_new_img'] = $db_img_path;
-        $record['product_content'] = $data['prompt'];
-        $record['template_id'] = $data['template_id'];
-        $record['createTime'] = date('Y-m-d H:i:s');
-        Db::name('product_image')->insert($record);
-
-        if (!empty($data['task_id'])) {
-            try {
-                $redis = getTaskRedis();
-                $redis->set("img_to_img_task:" . $data['task_id'], json_encode([
-                    'status' => 'completed',
-                    'image' => $db_img_path,
-                    'image_url' => $db_img_path,
-                    'completed_at' => date('Y-m-d H:i:s')
-                ], JSON_UNESCAPED_UNICODE), ['EX' => 300]);
-            } catch (\Exception $e) {
-                // 忽略 Redis 错误
+            $template_file = 'template-' . uniqid() . '.' . $template_img_ext;
+            $template_image_data = base64_decode($template_base64Data);
+            if ($template_image_data === false || !file_put_contents($saveDir . $template_file, $template_image_data)) {
+                $setTaskError('模板图保存失败');
+                return ['code' => 1, 'msg' => '模板图保存失败'];
             }
-        }
+            $template_db_path = '/uploads/Product/' . date('Y-m-d') . '/' . $template_file;
 
-        return $db_img_path;
-    }
+            $fileName = uniqid() . '.png';
+            if (!file_put_contents($saveDir . $fileName, $imageData)) {
+                $setTaskError('生成图保存失败');
+                return ['code' => 1, 'msg' => '生成图保存失败'];
+            }
+            $db_img_path = '/uploads/Product/' . date('Y-m-d') . '/' . $fileName;
+
+            Db::name('product_image_generate')->insert([
+                'prompt' => $prompt,
+                'model' => $model,
+                'product_img' => $product_db_path,
+                'reference_image' => $template_db_path,
+                'generated_image' => $db_img_path,
+                'status_val' => $status_val,
+                'size' => $size,
+                'sys_id' => $sys_id,
+                'createTime' => $date,
+            ]);
+
+            if (!empty($data['task_id'])) {
+                try {
+                    $redis = getTaskRedis();
+                    $redis->set("img_to_img_task:" . $data['task_id'], json_encode([
+                        'status' => 'completed',
+                        'image' => $db_img_path,
+                        'image_url' => $db_img_path,
+                        'completed_at' => date('Y-m-d H:i:s')
+                    ], JSON_UNESCAPED_UNICODE), ['EX' => 300]);
+                } catch (\Exception $e) {
+                    // 忽略 Redis 错误
+                }
+            }
+            return $db_img_path;
+        } else if($data['status_type'] == 'ProductTemplateReplace'){
+            $record = [];
+            // 获取产品信息
+            $product = Db::name('product')->where('id', $data['id'])->find();
+            if (empty($product)) {
+                $setTaskError('产品不存在');
+                return '产品不存在';
+            }
+            $product_code = $product['product_code'];
+            $product_code_prefix = substr($product_code, 0, 9); // 前九位
 
+            $rootPath = str_replace('\\', '/', ROOT_PATH);
+            // $saveDir = rtrim($rootPath, '/') . '/public/uploads/ceshi/';
+            $saveDir = rtrim($rootPath, '/') . '/public/uploads/merchant/'. '/' . $product_code_prefix . '/' . $product_code . '/' . 'newimg' . '/';
+
+            if (!is_dir($saveDir)) {
+                mkdir($saveDir, 0755, true);
+            }
+            $fileName = 'img2img-' . date('YmdHis') . '-' . uniqid() . '.png';
+            $fullPath = $saveDir . $fileName;
+            if (!file_put_contents($fullPath, $imageData)) {
+                $setTaskError('图片保存失败');
+                return ['code' => 1, 'msg' => '图片保存失败'];
+            }
+
+            // $db_img_path = '/uploads/ceshi/'. $fileName;
+            $db_img_path = '/uploads/merchant/'. '/' . $product_code_prefix . '/' . $product_code . '/' . 'newimg' . '/' . $fileName;
+
+            Db::name('product')->where('id',  $data['id'])->update
+            (
+                [
+                    'createTime' => date('Y-m-d H:i:s'),
+                    'content' => $data['prompt'],
+                    'product_new_img' => $db_img_path
+                ]
+            );
+            //生成新图后保存到记录 存留历史图片
+            $record['product_id'] = $data['id'];
+            $record['product_new_img'] = $db_img_path;
+            $record['product_content'] = $data['prompt'];
+            $record['template_id'] = $data['template_id'];
+            $record['createTime'] = date('Y-m-d H:i:s');
+            Db::name('product_image')->insert($record);
+
+            if (!empty($data['task_id'])) {
+                try {
+                    $redis = getTaskRedis();
+                    $redis->set("img_to_img_task:" . $data['task_id'], json_encode([
+                        'status' => 'completed',
+                        'image' => $db_img_path,
+                        'image_url' => $db_img_path,
+                        'completed_at' => date('Y-m-d H:i:s')
+                    ], JSON_UNESCAPED_UNICODE), ['EX' => 300]);
+                } catch (\Exception $e) {
+                    // 忽略 Redis 错误
+                }
+            }
+
+            return $db_img_path;
+        }else{
+            return ['code' => 1, 'msg' => '当前页面未进行配置,请联系管理员开通权限'];
+        }
+    }
 
     public function ImageToImage($fileName, $outputDirRaw, $new_image_url, $width, $height)
     {
@@ -300,7 +405,6 @@ class ImageToImageJob{
 
         // 调用 AI 图生图 API
         $ai = new AIGatewayService();
-        // $res = $ai->imgtoimgGptApi('', $new_image_url);
         $res = $ai->txt2imgWithControlNet('', $new_image_url);
         if (!isset($res['code']) || $res['code'] !== 0) {
             return json(['code' => 1, 'msg' => $res['msg'] ?? '图像生成失败']);

+ 83 - 73
application/job/TextToImageJob.php

@@ -34,10 +34,10 @@ class TextToImageJob
                 $job->delete();
                 return;
             }
-            
+
             //连接Redis(配置见 application/extra/queue.php)
             $redis = getTaskRedis();
-            
+
             echo "\n" . date('Y-m-d H:i:s') . " 任务开始:{$taskId}\n";
 
             // 更新任务状态为处理中
@@ -45,7 +45,7 @@ class TextToImageJob
                 'status' => 'processing',
                 'started_at' => date('Y-m-d H:i:s')
             ]), ['EX' => 300]); // 5分钟过期
- 
+
             try {
                 // 执行图片生成
                 $result = $this->get_txt_to_img($data);
@@ -57,12 +57,12 @@ class TextToImageJob
                     'image_url' => $result,
                     'completed_at' => date('Y-m-d H:i:s')
                 ]), ['EX' => 300]); // 5分钟过期
-                
+
                 echo "🎉 任务 {$taskId} 执行完成,图片生成成功!\n";
                 $job->delete();
             } catch (\Exception $e) {
                 echo "❌ 任务执行失败:" . $e->getMessage() . "\n";
-                
+
                 // 检查是否是网络超时错误
                 if (strpos($e->getMessage(), 'Connection timed out') !== false) {
                     // 对于超时错误,保持任务状态为处理中,让前端继续查询
@@ -214,7 +214,7 @@ class TextToImageJob
     }
 
     public function get_txt_to_img($data){
-
+        $status_val = trim($data['status_val']);
         $prompt = trim($data['prompt']);
         $model = trim($data['model']);
         $size = trim($data['size']);
@@ -224,87 +224,97 @@ class TextToImageJob
         if (empty($product)) {
             return '产品不存在';
         }
-        $product_code = $product['product_code'];
-        $product_code_prefix = substr($product_code, 0, 9); // 前九位
-
-
-        // 构建URL路径(使用正斜杠)
-        $url_path = '/uploads/merchant/' . $product_code_prefix . '/' . $product_code . '/newimg/';
-        // 构建物理路径(使用正斜杠确保统一格式)
-        $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'merchant' . '/' . $product_code_prefix . '/' . $product_code . '/' . 'newimg' . '/';
-        // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
-        $save_path = str_replace('\\', '/', $save_path);
-        // 自动创建文件夹(如果不存在)
-        if (!is_dir($save_path)) {
-            mkdir($save_path, 0755, true);
-        }
 
         // 调用AI生成图片
         $aiGateway = new AIGatewayService();
-        $res = $aiGateway->callDalleApi($prompt, $model, $size);
-        // 提取base64图片数据
-//        if (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
-//            $text_content = $res['candidates'][0]['content']['parts'][0]['text'];
-//            $text_content = $res['candidates'][0]['content']['parts'][0]['inlineData']['data'];
-//            // 匹配base64图片数据
-//            preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $text_content, $matches);
-
-        // 提取base64图片数据
-        $text_content = $res['candidates'][0]['content']['parts'][0]['inlineData']['data'];
-        $str = 'data:image/jpeg;base64,';
-        $text_content = $str. $text_content;
-        // 匹配base64图片数据
-        preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $text_content, $matches);
-            if (empty($matches)) {
-                return '未找到图片数据';
+        $res = $aiGateway->buildRequestData($status_val,$model,$prompt,$size);
+
+        // 提取base64图片数据,兼容两种返回格式
+        $base64_data = null;
+        $image_type = 'png';
+        // 新格式:data[0].b64_json 或 data[0].url
+        if (isset($res['data'][0]['b64_json']) && $res['data'][0]['b64_json']) {
+            $base64_data = preg_replace('/\s+/', '', $res['data'][0]['b64_json']);
+        } elseif (isset($res['data'][0]['url']) && $res['data'][0]['url']) {
+            $imageContent = file_get_contents($res['data'][0]['url']);
+            $base64_data = base64_encode($imageContent);
+        }
+        // 旧格式:candidates[0].content.parts[0].inlineData.data 或 text
+        elseif (isset($res['candidates'][0]['content']['parts'][0]['inlineData']['data'])) {
+            $text_content = $res['candidates'][0]['content']['parts'][0]['inlineData']['data'];
+            // 带前缀 data:image/xxx;base64, 或 裸 base64 都能识别
+            if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,(.+)$/is', $text_content, $matches)) {
+                $image_type = ($matches[1] == 'jpg' ? 'jpeg' : $matches[1]);
+                $base64_data = preg_replace('/\s+/', '', $matches[2]);
+            } else {
+                $base64_data = preg_replace('/\s+/', '', $text_content);
             }
-            $image_type = $matches[1];
-            $base64_data = $matches[2];
-
-            // 解码base64数据
-            $image_data = base64_decode($base64_data);
-            if ($image_data === false) {
-                return '图片解码失败';
+        } elseif (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
+            $text_content = $res['candidates'][0]['content']['parts'][0]['text'];
+            if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,(.+)$/is', $text_content, $matches)) {
+                $image_type = ($matches[1] == 'jpg' ? 'jpeg' : $matches[1]);
+                $base64_data = preg_replace('/\s+/', '', $matches[2]);
+            } else {
+                $base64_data = preg_replace('/\s+/', '', $text_content);
             }
+        }
 
-            // 生成唯一文件名(包含扩展名)
-            $file_name = uniqid() . '.' . $image_type;
-            $full_file_path = $save_path . $file_name;
+        if (empty($base64_data)) {
+            return '未找到图片数据';
+        }
 
-            // 保存图片到文件系统
-            if (!file_put_contents($full_file_path, $image_data)) {
-                return '图片保存失败';
-            }
+        $image_data = base64_decode($base64_data);
+        if ($image_data === false) {
+            return '图片解码失败';
+        }
 
-            // 生成数据库存储路径(使用正斜杠格式)
-            $db_img_path = $url_path . $file_name;
-
-            Db::name('product')->where('id',  $data['id'])->update
-            (
-                [
-                'createTime' => date('Y-m-d H:i:s'),
-                'content' => $data['prompt'],
-                'product_new_img' => $db_img_path
-                ]
-            );
-            //生成新图后保存到记录 存留历史图片
-            $record['product_id'] = $data['id'];
-            $record['product_new_img'] = $db_img_path;
-            $record['product_content'] = $data['prompt'];
-            $record['template_id'] = $data['template_id'];
-            $record['createTime'] = date('Y-m-d H:i:s');
-            Db::name('product_image')->insert($record);
-
-            return $db_img_path;
+        $rootPath = str_replace('\\', '/', ROOT_PATH);
+        $relDir = '/uploads/ceshi/';
+        // 生产:$code = $product['product_code'];
+        // 生产:$relDir = '/uploads/merchant/' . substr($code, 0, 9) . '/' . $code . '/newimg/';
+        $saveDir = rtrim($rootPath, '/') . '/public' . $relDir;
+        if (!is_dir($saveDir)) {
+            mkdir($saveDir, 0755, true);
+        }
+
+        $file_name = uniqid() . '.' . $image_type;
+        if (!file_put_contents($saveDir . $file_name, $image_data)) {
+            return '图片保存失败';
+        }
+        $db_img_path = $relDir . $file_name;
+        echo "<pre>";
+        print_r($db_img_path);
+        echo "<pre>";die;
+        Db::name('product')->where('id', $data['id'])->update([
+            'createTime' => date('Y-m-d H:i:s'),
+            'content' => $data['prompt'],
+            'product_new_img' => $db_img_path
+        ]);
+        $record = [
+            'product_id' => $data['id'],
+            'product_new_img' => $db_img_path,
+            'product_content' => $data['prompt'],
+            'template_id' => $data['template_id'] ?? 0,
+            'createTime' => date('Y-m-d H:i:s'),
+        ];
+        Db::name('product_image')->insert($record);
+
+        return $db_img_path;
     }
 
     /**
      * 文生图处理函数
      * 描述:根据提示词调用图像生成接口,保存图像文件,并更新数据库
      */
-    public function textToImage($fileName, $outputDirRaw, $width, $height, $prompt, $img_name, $selectedOption,$executeKeywords,$sourceDir)
+    public function textToImage($data,$prompt,$img_name)
     {
-
+        $fileName = $data["file_name"];
+        $outputDirRaw = $data["outputDir"];
+        $width = $data["width"];
+        $height = $data["height"];
+        $selectedOption = $data["selectedOption"];
+        $executeKeywords = $data["executeKeywords"];
+        $sourceDir = $data["sourceDir"];
 
         $rootPath = str_replace('\\', '/', ROOT_PATH);
         $outputDir = rtrim($rootPath . 'public/' . $outputDirRaw, '/') . '/';
@@ -357,7 +367,7 @@ class TextToImageJob
 
         // AI 图像生成调用
         $ai = new AIGatewayService();
-        $response = $ai->callDalleApi($template['content'].$prompt, $selectedOption);
+        $response = $ai->buildRequestData('文生图',$template['content'].$prompt, $selectedOption);
 
         if (isset($response['error'])) {
             throw new \Exception("❌ 图像生成失败:" . $response['error']['message']);

+ 134 - 99
application/job/TextToTextJob.php

@@ -5,144 +5,179 @@ use think\Db;
 use think\queue\Job;
 use think\Queue;
 /**
- * 文生文任务队列处理类
- * 基于图生文生成的描述,继续生成扩展文本内容(链式任务中间环)
+ * 文生文
  */
 class TextToTextJob
 {
     /**
      * 队列任务入口
-     * @param Job $job 当前队列任务
-     * @param array $data 包含任务上下文、路径信息、链式类型等
      */
     public function fire(Job $job, $data)
     {
-
-        $logId = $data['log_id'] ?? null;
-        echo "━━━━━━━━━━ ▶ 文生文任务开始处理━━━━━━━━━━\n";
-
-        try {
-            $startTime = date('Y-m-d H:i:s');
-
-            if ($logId) {
-                Db::name('image_task_log')->where('id', $logId)->update([
-                    'status' => 1,
-                    'log' => '文生文处理中',
-                    'update_time' => date('Y-m-d H:i:s')
-                ]);
+        if (isset($data['status_val']) && $data['status_val'] == '文生文') {
+            $taskId = $data['task_id'];
+            $redis = getTaskRedis();
+            $existing = $redis->get("txt_to_txt_task:{$taskId}");
+            if ($existing) {
+                $info = json_decode($existing, true);
+                if (isset($info['status']) && $info['status'] === 'completed') {
+                    echo "任务 {$taskId} 已完成,跳过重复执行\n";
+                    $job->delete();
+                    return;
+                }
             }
+            try {
+                echo " 开始处理文生文".date('Y-m-d H:i:s')."\n";
 
-            $fullPath = rtrim($data['sourceDir'], '/') . '/' . ltrim($data['file_name'], '/');
-
-            $list = Db::name("text_to_image")
-                ->where('old_image_url', $fullPath)
-                ->where('img_name', '<>', '')
-//                ->where('status', 0)
-                ->select();
+                $result = $this->get_txt_to_txt($data);
+                if (is_array($result) && isset($result['code']) && $result['code'] !== 0) {
+                    throw new \Exception($result['msg'] ?? '文生文失败');
+                }
 
-            if (!empty($list)) {
-                foreach ($list as $index => $row) {
-                    $currentTime = date('Y-m-d H:i:s');
-                    echo "处理时间:{$currentTime}\n";
-                    echo "👉 正在处理第 " . ($index + 1) . " 条,ID: {$row['id']}\n";
+                echo "🎉 任务 {$taskId} 执行完成,文生文生成成功!\n";
+                echo "结束时间:" . date('Y-m-d H:i:s') . "\n";
+                $job->delete();
 
-                    $result = $this->textToTxt($data,$row['id'],$data["txttotxt_selectedOption"],$fullPath,$data['sourceDir']);
-                    echo $result;
-                    echo "✅ 处理结果:完成\n";
-                    echo "完成时间:" . date('Y-m-d H:i:s') . "\n";
+            } catch (\Exception $e) {
+                echo "文生文失败: " . $e->getMessage() . "\n";
+                $job->delete();
+            }
+            $job->delete();
+        }else{
 
-                    echo "Processed: " . static::class . "\n\n";
-                }
+            $logId = $data['log_id'] ?? null;
+            echo " 开始处理文生文".date('Y-m-d H:i:s')."\n";
 
-                // 更新日志为成功
+            try {
                 if ($logId) {
                     Db::name('image_task_log')->where('id', $logId)->update([
-                        'status' => 2,
-                        'log' => '文生文处理成功',
+                        'status' => 1,
+                        'log' => '文生文处理',
                         'update_time' => date('Y-m-d H:i:s')
                     ]);
                 }
 
-                echo "处理完成\n";
-            } else {
-                echo "⚠ 未找到可处理的数据,跳过执行\n";
+                $fullPath = rtrim($data['sourceDir'], '/') . '/' . ltrim($data['file_name'], '/');
+                $list = Db::name("text_to_image")
+                    ->where('old_image_url', $fullPath)
+                    ->where('img_name', '<>', '')
+                    ->select();
+
+                if (!empty($list)) {
+                    foreach ($list as $index => $row) {
+                        $currentTime = date('Y-m-d H:i:s');
+                        echo "处理时间:{$currentTime}\n";
+                        echo "👉 正在处理第 " . ($index + 1) . " 条,ID: {$row['id']}\n";
+
+                        $result = $this->get_txt_to_txt($data,$row['id']);
+                        echo $result;
+                        echo "✅ 处理结果:完成\n";
+                        echo "完成时间:" . date('Y-m-d H:i:s') . "\n";
+
+                        echo "Processed: " . static::class . "\n\n";
+                    }
+
+                    // 更新日志为成功
+                    if ($logId) {
+                        Db::name('image_task_log')->where('id', $logId)->update([
+                            'status' => 2,
+                            'log' => '文生文处理成功',
+                            'update_time' => date('Y-m-d H:i:s')
+                        ]);
+                    }
+
+                    echo "处理完成\n";
+                } else {
+                    echo "⚠ 未找到可处理的数据,跳过执行\n";
+
+                    if ($logId) {
+                        Db::name('image_task_log')->where('id', $logId)->update([
+                            'status' => 2,
+                            'log' => '无数据可处理,已跳过',
+                            'update_time' => date('Y-m-d H:i:s')
+                        ]);
+                    }
+                }
+
+                // 链式执行:检查是否还有下一个任务
+                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) {
+                echo "❌ 错误信息: " . $e->getMessage() . "\n";
+                echo "📄 文件: " . $e->getFile() . ",第 " . $e->getLine() . " 行\n";
 
                 if ($logId) {
                     Db::name('image_task_log')->where('id', $logId)->update([
-                        'status' => 2,
-                        'log' => '无数据可处理,已跳过',
+                        'status' => -1,
+                        'log' => '文生文失败:' . $e->getMessage(),
                         'update_time' => date('Y-m-d H:i:s')
                     ]);
                 }
             }
-
-            // 链式执行:检查是否还有下一个任务
-            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";
-
-            if ($logId) {
-                Db::name('image_task_log')->where('id', $logId)->update([
-                    'status' => -1,
-                    'log' => '文生文失败:' . $e->getMessage(),
-                    'update_time' => date('Y-m-d H:i:s')
-                ]);
-            }
+            $job->delete();
         }
-        $job->delete();
+
     }
 
 
     /**
      * 文生文核心处理逻辑(调用 GPT 接口)
-     * @param array $data 任务数据
-     * @param int $id text_to_image 表主键
-     * @param string $txttotxt_selectedOption 文生文模型
-     * @param string $fullPath 完整路径
-     * @param string $sourceDir 源目录
      * @return string
      */
-    public function textToTxt($data, $id, $txttotxt_selectedOption, $fullPath, $sourceDir)
+    public function get_txt_to_txt($data,$id)
     {
-        $template = Db::name('template')
-            ->field('id,english_content,content')
-            ->where('path', $sourceDir)
-            ->where('ids', 1)
-            ->find();
-
-        $record = Db::name('text_to_image')
-            ->field('id,english_description,chinese_description')
-            ->where('id', $id)
-            ->order('id desc')
-            ->find();
-
-        if (!$record) {
-            return '没有找到匹配的图像记录';
-        }
-
-        // 拼接提示词调用文生文接口
         $ai = new AIGatewayService();
-        $prompt = $template['english_content'] . $record['chinese_description'];
-        $gptRes = $ai->txtGptApi($prompt, $txttotxt_selectedOption);
-        $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
+        if ($data['status_type'] == 'ProductImageGeneration') {
+            $content = "你是专业的产品文案优化师,仅返回优化后的文案,无任何多余内容:
+                        优化规则:
+                        1. 语气:亲切自然,符合电商产品描述风格;
+                        2. 结构:突出产品卖点,逻辑清晰;
+                        3. 禁忌:不添加无关形容词,不修改产品核心信息;
+                        需要优化的文案:";
+            $prompt = $content. $data['prompt'];
+            $gptRes = $ai->buildRequestData($data['model'],$data['status_val'],$prompt);
+            $gptText = trim($gptRes['choices'][0]['message']['content']);
+            return $gptText;
+        }else if($data['status_type'] == 'ProductTemplateReplace'){
+            $template = Db::name('template')
+                ->field('id,english_content,content')
+                ->where('path', $data['sourceDir'])
+                ->where('ids', 1)
+                ->find();
+
+            $record = Db::name('text_to_image')
+                ->field('id,english_description,chinese_description')
+                ->where('id', $id)
+                ->order('id desc')
+                ->find();
+
+            if (!$record) {
+                return '没有找到匹配的图像记录';
+            }
 
-        // 更新数据库记录
-        Db::name('text_to_image')->where('id', $record['id'])->update([
-            'english_description' => $gptText,
-            'status_name' => "文生文"
-        ]);
+            // 拼接提示词调用文生文接口
+            $prompt = $template['english_content'] . $record['chinese_description'];
+            $gptRes = $ai->buildRequestData($data['model'],$data['status_val'],$prompt);
+            $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
+
+            // 更新数据库记录
+            Db::name('text_to_image')->where('id', $record['id'])->update([
+                'english_description' => $gptText,
+                'status_name' => "文生文"
+            ]);
+            return 0;
+        }else{
+            return ['code' => 1, 'msg' => '当前页面未进行配置,请联系管理员开通权限'];
+        }
 
-        return 0;
     }
 
 }

+ 3 - 0
application/route.php

@@ -20,6 +20,9 @@ return [
     ],
     // 路由规则
     'user/diagrams_list/id/:id.html' => 'index/user/diagrams_list',
+    // 图生图接口 - 支持多种访问路径(与 error 控制器提示的 urls 一致)
+    'img2img' => 'api/Index/img_to_img',
+    'api/img2img' => 'api/Index/img_to_img',
     // 当请求被误解析为 error 时,返回友好提示
     'error/500' => 'error/Index/index',
     'error/:code' => 'error/Index/index',

File diff suppressed because it is too large
+ 522 - 557
application/service/AIGatewayService.php


+ 199 - 192
application/service/ImageService.php

@@ -1,44 +1,63 @@
 <?php
 namespace app\service;
+
 use app\service\AIGatewayService;
 use think\Db;
 use think\Exception;
 use think\Queue;
+
 /**
- * ImageService 类用于处理图像任务和存放日志队列。
- * 该类将前端传过来的多个图像信息推送到处理队列中。
+ * ImageService 图像任务服务
+ * 负责图生文、文生文、文生图、图生图等 AI 任务的队列派发与状态管理
  */
-class ImageService{
+class ImageService
+{
+    /** 队列名称 */
+    private const QUEUE_ARRIMAGE = 'arrimage';
+
+    /** Redis 任务状态 TTL(秒) */
+    private const TASK_TTL = 300;
+
+    /** Redis key 前缀 */
+    private const KEY_TEXT_TO_IMAGE = 'text_to_image_task:';
+    private const KEY_IMG_TO_IMG = 'img_to_img_task:';
 
     /**
-     * 直接调用图生文API并返回结果
-     * @param array $params 请求参数,包含文生文提示词、模型类型等
-     * @return array GPT生成的结果
+     * 图生文:提交到队列
+     * @param array $params 请求参数
+     * @return bool
      */
-    public function handleImgToText($params) {
-        Queue::push('app\job\ImageArrJob', $params, "arrimage");
+    public function handleImgToText(array $params): bool
+    {
+        Queue::push('app\job\ImageArrJob', $params, self::QUEUE_ARRIMAGE);
         return true;
     }
 
     /**
-     * 文生文
-     * @param array $params 请求参数,包含文生文提示词、模型类型等
-     * @return array GPT生成的结果
+     * 文生文:直接调用 API 并返回结果
+     * @param string $prompt 提示词
+     * @param string $model 模型名称
+     * @return array ['success'=>bool, 'message'=>string, 'data'=>string]
      */
-    public function handleTextToText($prompt,$model) {
+    public function handleTextToText($status_val, string $prompt, string $model): array
+    {
         $ai = new AIGatewayService();
-        $gptRes = $ai->txtGptApi($prompt, $model);
+        $gptRes = $ai->buildRequestData($status_val, $model, $prompt);
+
 
-        // 根据不同模型解析返回结果
         $gptText = '';
-        if (strpos($model, 'gemini') !== false && isset($gptRes['candidates'][0]['content']['parts'][0]['text'])) {
-            $gptText = trim($gptRes['candidates'][0]['content']['parts'][0]['text']);
+        if (isset($gptRes['candidates'][0]['content']['parts'][0]['text'])) {
+            $gptText = $gptRes['candidates'][0]['content']['parts'][0]['text'];
         } elseif (isset($gptRes['choices'][0]['message']['content'])) {
             $gptText = trim($gptRes['choices'][0]['message']['content']);
-        } else {
-            throw new Exception('AI API返回格式错误');
         }
-
+        if (isset($gptRes['error']) || isset($gptRes['code']) && $gptRes['code'] !== 0) {
+            return [
+                'success' => false,
+                'message' => $gptRes['msg'] ?? $gptRes['error']['message'] ?? '生成失败',
+                'data' => ''
+            ];
+        }
         return [
             'success' => true,
             'message' => '生成成功',
@@ -47,224 +66,212 @@ class ImageService{
     }
 
     /**
-     * 文生图API并返回结果
-     * @param array $params 请求参数,包含文生文提示词、模型类型
-     * @return array GPT生成的结果
+     * 文生图:创建任务并推送到队列
+     * @param array $params 含 id、model 
+     * @return array ['success'=>bool, 'message'=>string, 'task_id'=>string]
      */
-    public function handleTextToImg($params) {
-        // 生成唯一任务ID,格式:$params['id']-年月日时间-随机四位数字
-        $id = $params['id'];
-        $time = date('YmdHis');
-        $random = mt_rand(1000, 9999);
-        $taskId = "{$id}-{$time}-{$random}";
-        $params['task_id'] = $taskId;
-
-        // 将任务状态存储到Redis(配置见 application/extra/queue.php)
-        $redis = getTaskRedis();
-        $redis->set("text_to_image_task:{$taskId}", json_encode([
-            'status' => 'pending',
-            'created_at' => date('Y-m-d H:i:s')
-        ]), ['EX' => 300]); // 5分钟过期
+    public function handleTextToImg(array $params): array
+    {
+        return $this->submitTaskToQueue(
+            $params,
+            self::KEY_TEXT_TO_IMAGE,
+            '正在生成图片中,请稍等.....'
+        );
+    }
 
-        // 将任务推送到队列
-        Queue::push('app\job\ImageArrJob', $params, "arrimage");
-        // 返回任务ID
-        return ['success' => true, 'message' => '正在生成图片中,请稍等.....', 'task_id' => $taskId];
+    /**
+     * 图生图:创建任务并推送到队列(产品图+模板图)
+     * @param array $params 含 id、product_img、template_img、prompt、model 等
+     * @return array ['success'=>bool, 'message'=>string, 'task_id'=>string]
+     */
+    public function handleImgToImg(array $params): array
+    {
+        return $this->submitTaskToQueue(
+            $params,
+            self::KEY_IMG_TO_IMG,
+            '正在生成图片中,请稍等.....'
+        );
     }
 
     /**
-     * 图生图(Gemini:产品图+模板图)
-     * @param array $params 包含 product_img、template_img、prompt、model、id(可选)
-     * @return array
+     * 通用:创建任务 ID、写入 Redis、推送到队列
      */
-    public function handleImgToImg($params) {
-        $id = $params['id'];
-        $time = date('YmdHis');
-        $random = mt_rand(1000, 9999);
-        $taskId = "{$id}-{$time}-{$random}";
+    private function submitTaskToQueue(array $params, string $redisKeyPrefix, string $message): array
+    {
+        $taskId = ($params['id'] ?? '0') . '-' . date('YmdHis') . '-' . mt_rand(1000, 9999);
         $params['task_id'] = $taskId;
 
         $redis = getTaskRedis();
-        $redis->set("img_to_img_task:{$taskId}", json_encode([
+        $redis->set($redisKeyPrefix . $taskId, json_encode([
             'status' => 'pending',
             'created_at' => date('Y-m-d H:i:s')
-        ]), ['EX' => 300]);
+        ]), ['EX' => self::TASK_TTL]);
 
-        Queue::push('app\job\ImageArrJob', $params, "arrimage");
-        return ['success' => true, 'message' => '正在生成图片中,请稍等.....', 'task_id' => $taskId];
-    }
+        Queue::push('app\job\ImageArrJob', $params, self::QUEUE_ARRIMAGE);
 
+        return [
+            'success' => true,
+            'message' => $message,
+            'task_id' => $taskId
+        ];
+    }
 
     /**
-     * 推送图像任务到队列(支持链式和单独模式)
-     * @param array $params 请求参数,包含图像批次、模型类型、尺寸等
+     * 批量图像任务:支持链式任务和单类型任务
+     * @param array $params 含 batch、num、type、old_image_file 等
+     * @return bool
      */
-    public function handleImage($params) {
-
-        if (!isset($params["batch"])) {return false;}
-
-        $arr = [];
-        // 获取图像批量信息
-        $batch = $params["batch"];
-        // 获取执行次数数量
-        $num = $params["num"];
-
-        /*获取ids为1模板
-         * english_content 文生文提示词
-         * content 图生文提示词
-         * */
-        $template = Db::name('template')
-            ->field('id,english_content,content,ids')
-            ->where('path',$params['old_image_file'])
-            ->where('ids',1)
-            ->find();
+    public function handleImage(array $params): bool
+    {
+        if (!isset($params['batch']) || !is_array($params['batch'])) {
+            return false;
+        }
 
+        $arr = $this->buildBatchItems($params);
+        $insertData = $this->buildQueueLogData($params, count($arr));
 
+        if (empty($params['type'])) {
+            $this->dispatchFullChainTask($arr, $insertData);
+        } else {
+            $result = $this->dispatchSingleTypeTask($arr, $params, $insertData);
+            if (!$result) {
+                return false;
+            }
+        }
+        return true;
+    }
 
-        // 构建任务基础结构(每图生成 N 份任务)
-        foreach ($batch as $k => $v) {
+    /** 构建批量任务项 */
+    private function buildBatchItems(array $params): array
+    {
+        $arr = [];
+        foreach ($params['batch'] as $v) {
             $baseItem = [
-                "sourceDir" => $this->sourceDir($v, 1),
-                "outputDir" => $this->sourceDir($v, 2),
-                "file_name" => $this->sourceDir($v, 3),
-                "type" => $params['type'] ?? '',
-                "selectedOption" => $params['selectedOption'],//文生图模型
-                "txttotxt_selectedOption" => $params['txttotxt_selectedOption'],//文生文模型
-                "imgtotxt_selectedOption" => $params['imgtotxt_selectedOption'],//图生文模型
-                "prompt" => '',
-                "width" => $params['width'],
-                "height" => $params['height'],
-                "executeKeywords" => $params['executeKeywords'],//是否执行几何图
-                "sys_id" => $params['sys_id']//用户
+                'sourceDir' => $this->sourceDir($v, 1),
+                'outputDir' => $this->sourceDir($v, 2),
+                'file_name' => $this->sourceDir($v, 3),
+                'type' => $params['type'] ?? '',
+                'selectedOption' => $params['selectedOption'] ?? '',
+                'txttotxt_selectedOption' => $params['txttotxt_selectedOption'] ?? '',
+                'imgtotxt_selectedOption' => $params['imgtotxt_selectedOption'] ?? '',
+                'prompt' => '',
+                'width' => $params['width'] ?? 0,
+                'height' => $params['height'] ?? 0,
+                'executeKeywords' => $params['executeKeywords'] ?? '',
+                'sys_id' => $params['sys_id'] ?? ''
             ];
-            // 创建$num个相同的项目并合并到$arr
-            $arr = array_merge($arr, array_fill(0, $num, $baseItem));
+            $num = (int)($params['num'] ?? 1);
+            $arr = array_merge($arr, array_fill(0, max(1, $num), $baseItem));
         }
+        return $arr;
+    }
 
-        // 插入队列日志
-        $insertData = [
-            'create_time'  => date('Y-m-d H:i:s'),
-            'old_image_file'       => $params['old_image_file'],
-            'status'       => '等待中',
-            'image_count'  => count($arr),
-            'params'       => json_encode($params, JSON_UNESCAPED_UNICODE)
+    /** 构建队列日志插入数据 */
+    private function buildQueueLogData(array $params, int $imageCount): array
+    {
+        return [
+            'create_time' => date('Y-m-d H:i:s'),
+            'old_image_file' => $params['old_image_file'] ?? '',
+            'status' => '等待中',
+            'image_count' => $imageCount,
+            'params' => json_encode($params, JSON_UNESCAPED_UNICODE)
         ];
+    }
 
-        //模型任务类型处理
-        if (empty($params['type'])) {
-            /*
-             * 执行全部任务时一键链式任务队列
-             * 用于存放队列日志
-             * 链式任务:图生文 → 文生文 → 文生图
-             * */
-            $insertData['model'] = "gpt-4-vision-preview,"."gpt-4,".$params['selectedOption'];
-            $insertData['model_name'] = '文生图';
-            $task_id = Db::name('queue_logs')->insertGetId($insertData);
-
-            $arr = array_map(function ($item) use ($task_id) {
-                $item['type'] = '图生文';
-                $item['chain_next'] = ['文生文', '文生图','图生图','高清放大'];
-                $item['task_id'] = $task_id;
-                return $item;
-            }, $arr);
-
-            $payload = [
-                'task_id' => $task_id,
-                'data' => $arr
-            ];
+    /** 派发一键链式任务:图生文→文生文→文生图→图生图→高清放大 */
+    private function dispatchFullChainTask(array $arr, array $insertData): void
+    {
+        $params = json_decode($insertData['params'], true) ?: [];
+        $insertData['model'] = 'gpt-4-vision-preview,gpt-4,' . ($params['selectedOption'] ?? '');
+        $insertData['model_name'] = '文生图';
 
-            Queue::push('app\job\ImageArrJob', $payload, "arrimage");
-        } else {
-            // 指定单个独立任务类型
-            switch ($params['type']) {
-                case '图生文':
-                    $insertData['model'] = 'gpt-4-vision-preview';
-                    $insertData['model_name'] = '图生文';
-                    break;
-                case '文生文':
-                    $insertData['model'] = $params['txttotxt_selectedOption'];
-                    $insertData['model_name'] = '文生文';
-                    break;
-                case '文生图':
-                    $insertData['model'] = $params['selectedOption'];
-                    $insertData['model_name'] = '文生图';
-                    break;
-                case '图生图':
-                    $insertData['model'] = "realisticVisionV51_v51VAE-inpainting.safetensors [f0d4872d24]";
-                    $insertData['model_name'] = '图生图';
-                    break;
-                case '高清放大':
-                    $insertData['model'] = "高清放大";
-                    $insertData['model_name'] = '高清放大';
-                    break;
-                default:
-                    return false;
-            }
+        $taskId = Db::name('queue_logs')->insertGetId($insertData);
+        $arr = array_map(function ($item) use ($taskId) {
+            $item['type'] = '图生文';
+            $item['chain_next'] = ['文生文', '文生图', '图生图', '高清放大'];
+            $item['task_id'] = $taskId;
+            return $item;
+        }, $arr);
 
-            //将一组队列存放queue_logs任务表中,将新增id最为任务id记录
-            $task_id = Db::name('queue_logs')->insertGetId($insertData);
-            $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");
+        Queue::push('app\job\ImageArrJob', ['task_id' => $taskId, 'data' => $arr], self::QUEUE_ARRIMAGE);
+    }
+
+    /** 派发单类型任务 */
+    private function dispatchSingleTypeTask(array $arr, array $params, array $insertData): bool
+    {
+        $typeConfig = $this->getTypeConfig($params['type'], $params);
+        if (!$typeConfig) {
+            return false;
         }
+
+        $insertData['model'] = $typeConfig['model'];
+        $insertData['model_name'] = $typeConfig['model_name'];
+
+        $taskId = Db::name('queue_logs')->insertGetId($insertData);
+        $arr = array_map(function ($item) use ($params, $taskId) {
+            $item['type'] = $params['type'];
+            $item['task_id'] = $taskId;
+            return $item;
+        }, $arr);
+
+        Queue::push('app\job\ImageArrJob', ['task_id' => $taskId, 'data' => $arr], self::QUEUE_ARRIMAGE);
         return true;
     }
 
+    /** 任务类型对应的 model 配置 */
+    private function getTypeConfig(string $type, array $params = []): ?array
+    {
+        $configs = [
+            '图生文' => ['model' => 'gpt-4-vision-preview', 'model_name' => '图生文'],
+            '文生文' => ['model_key' => 'txttotxt_selectedOption', 'model_name' => '文生文'],
+            '文生图' => ['model_key' => 'selectedOption', 'model_name' => '文生图'],
+            '图生图' => ['model' => 'realisticVisionV51_v51VAE-inpainting.safetensors [f0d4872d24]', 'model_name' => '图生图'],
+            '高清放大' => ['model' => '高清放大', 'model_name' => '高清放大']
+        ];
+
+        $cfg = $configs[$type] ?? null;
+        if (!$cfg) {
+            return null;
+        }
+        if (isset($cfg['model_key'])) {
+            $cfg['model'] = $params[$cfg['model_key']] ?? '';
+            unset($cfg['model_key']);
+        }
+        return $cfg;
+    }
+
     /**
-     * 解析图像路径,返回不同组成部分
-     *
-     * @param string $filePath 图像路径(如 uploads/operate/ai/Preview/20240610/xxx.png)
-     * @param int $type 返回内容类型:
-     *        1 = 基础路径(去掉日期+文件名)
-     *          sourceDir 源目录uploads/operate/ai/Preview/
-     *        2 = 输出路径(Preview 替换为 dall-e,并加日期代表当天数据存到当天文件夹中)
-     *          outputDir 输出目录/uploads/operate/ai/dall-e/hua/并加日期
-     *        3 = 文件名
-     *          file_name 文件名0194b6fdd6203fda369d5e3b74b6b454.png
+     * 解析图像路径
+     * @param string $filePath 如 uploads/operate/ai/Preview/20240610/xxx.png
+     * @param int $type 1=源目录 2=输出目录 3=文件名
      * @return string|null
      */
-    public function sourceDir($filePath, $type) {
-        $arr = [];
-
+    public function sourceDir(string $filePath, int $type): ?string
+    {
         $pathParts = explode('/', $filePath);
-        $filename = array_pop($pathParts); // 最后是文件名
-
+        $filename = array_pop($pathParts);
         $baseParts = $pathParts;
-
-        // 查找是否有 8 位数字(即日期)文件夹
         $date = '';
+
         foreach ($pathParts as $index => $part) {
             if (preg_match('/^\d{8}$/', $part)) {
                 $date = $part;
-                unset($baseParts[$index]); // 日期不算在 basePath 里
+                unset($baseParts[$index]);
                 break;
             }
         }
 
-        $arr = [
-            'basePath' => implode('/', $baseParts),
-            'date' => $date,
-            'filename' => $filename
-        ];
+        $basePath = implode('/', $baseParts);
 
-        // 根据类型返回不同路径
-        if ($type == 1) {
-            return $arr["basePath"];
-        }
-        if ($type == 2) {
-            return '/' . str_replace('/Preview/', '/dall-e/', $arr["basePath"]) . $arr["date"];
-        }
-        if ($type == 3) {
-            return $arr["filename"];
+        switch ($type) {
+            case 1:
+                return $basePath;
+            case 2:
+                return '/' . str_replace('/Preview/', '/dall-e/', $basePath) . $date;
+            case 3:
+                return $filename;
+            default:
+                return null;
         }
     }
 }

Some files were not shown because too many files changed in this diff