liuhairui hai 1 semana
pai
achega
37ade597a2

+ 362 - 184
application/api/controller/Index.php

@@ -187,210 +187,227 @@ class Index extends Api
         }
     }
 
+
     /**
-     * 图生图本地测试
-     * 原图+参考图+提示词=生成新图
-     * 已替换为公开测试图(免本地文件)+ 通用提示词
+     * 图生图: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(可选)
      */
-    public function GET_ImgToImg()
+    public function img_to_img()
     {
-        // 【通用简化提示词】:明确图生图核心需求,适配测试场景
-        $prompt = '参考第二张模板图片的轻奢设计风格、光影布局、纯色背景和产品展示比例,将第一张可乐产品图的主体替换到模板的核心位置,保留模板的所有非产品设计元素,生成1:1高清产品效果图,无水印、无多余文字,画质清晰';
-
-        $size = '1:1';// 生成图片比例
-        $model = 'gemini-3-pro-image-preview'; // 当前使用的模型
-        $numImages = 1; // 生成图像数量,gemini模型目前只支持生成1张
-
-        // 支持多图像生成的模型:dall-e-3, black-forest-labs/FLUX.1-kontext-pro, gpt-image-1
-        // 这些模型通过设置 'n' 参数来指定生成图像数量
-
-        // 检查模型是否支持多图像生成
-        $supportMultiImages = in_array($model, ['dall-e-3', 'black-forest-labs/FLUX.1-kontext-pro', 'gpt-image-1']);
-        if ($supportMultiImages && $numImages > 1) {
-            // 对于支持多图像生成的模型,可以设置生成数量
-            $n = $numImages;
-        }
-        /**************************
-         * 替换为:公开测试图(直接转Base64,无需本地文件)
-         * 图1:产品图(可乐罐,通用测试)
-         * 图2:模版图(轻奢产品展示背景,通用测试)
-         **************************/
-            // 产品图:公开可乐图URL
-            $productImgUrl = 'https://s41.ax1x.com/2026/02/02/pZ4crcj.jpg';
-            // 模版图:公开轻奢产品展示背景图URL
-            $templateImgUrl = 'https://s41.ax1x.com/2026/02/02/pZ4cw4S.jpg';
-
-            // 封装:URL转纯Base64(去前缀)+ MIME类型,无需本地文件,直接测试
-            function urlToPureBase64($imgUrl) {
-                $imgContent = file_get_contents($imgUrl);
-                if (!$imgContent) throw new Exception("图片URL读取失败");
-
-                // 通过文件扩展名确定MIME类型
-                $extension = strtolower(pathinfo($imgUrl, PATHINFO_EXTENSION));
-                $mimeTypes = [
-                    'jpg' => 'image/jpeg',
-                    'jpeg' => 'image/jpeg',
-                    'png' => 'image/png',
-                    'gif' => 'image/gif',
-                    'webp' => 'image/webp'
-                ];
-                $mime = $mimeTypes[$extension] ?? 'image/jpeg';
-
-                $base64 = base64_encode($imgContent);
-                return ['mime' => $mime, 'base64' => $base64];
+        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]);
             }
 
-        try {
-            // 获取两张测试图的纯Base64和MIME
-            $productImg = urlToPureBase64($productImgUrl);
-            $templateImg = urlToPureBase64($templateImgUrl);
-
-            // ########## 构造API请求参数(适配gemini-3-pro-image-preview) ##########
-            $data = [
-                "contents" => [
-                    [
-                        "role" => "user",
-                        "parts" => [
-                            ["text" => $prompt],
-                            // 产品图
-                            ["inlineData" => [
-                                "mimeType" => $productImg['mime'],
-                                "data" => $productImg['base64']
-                            ]],
-                            // 模版图
-                            ["inlineData" => [
-                                "mimeType" => $templateImg['mime'],
-                                "data" => $templateImg['base64']
-                            ]]
-                        ]
+        // 将 /uploads/xxx 或 uploads/xxx 转为本地绝对路径
+        $productImgPath = $this->resolveImagePath($productImgRaw);
+        $templateImgPath = $this->resolveImagePath($templateImgRaw);
+
+        // ========== 3. 提示词 ==========
+        $prompt = $customPrompt ?: '请完成产品模板替换:
+        1. 从产品图提取产品主体、品牌名称、核心文案;
+        2. 从模板图继承版式布局、文字排版、色彩风格、背景元素;
+        3. 将模板图中的产品和文字替换为产品图的内容;
+        4. 最终生成的图片与模板图视觉风格100%统一,仅替换产品和文字。';
+
+        // ========== 4. 图片转Base64 ==========
+        $productImg = $this->img_to_base64($productImgPath);
+        if (isset($productImg['error'])) {
+            $this->json_response(['code' => 1, 'msg' => '[步骤1]产品图加载失败:' . $productImg['error'], 'data' => ['path' => $productImgRaw]]);
+        }
+
+        $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' => [
+                [
+                    'role' => 'user',
+                    'parts' => [
+                        ['text' => $prompt],
+                        ['inlineData' => ['mimeType' => $productImg['mime'], 'data' => $productImg['base64']]],
+                        ['inlineData' => ['mimeType' => $templateImg['mime'], 'data' => $templateImg['base64']]]
                     ]
-                ],
-                "generationConfig" => [
-                    "responseModalities" => ["IMAGE"],
-                    "imageConfig" => ["aspectRatio" => $size, "quality" => "HIGH"],
-                    "temperature" => 0.6,
-                    "topP" => 0.9,
-                    "maxOutputTokens" => 2048
                 ]
-            ];
+            ],
+            'generationConfig' => [
+                'responseModalities' => ['IMAGE'],
+                'imageConfig' => [
+                    'aspectRatio' => '5:4',
+                    'quality' => 'HIGH',
+                    'width' => 1000,
+                    'height' => 800
+                ],
+                'temperature' => 0.3,
+                'topP' => 0.8,
+                'maxOutputTokens' => 2048
+            ]
+        ];
 
-            // ########## 调用API ##########
-            $apiUrl = 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:generateContent';
-            $apiKey = 'sk-9aIV9nx7pJxJFMrB8REtNbhjYuNBxCcnEOwiJDHd6UwmN2eJ';
-            $result = AIGatewayService::callApi($apiUrl, $apiKey, $data);
-
-            // 处理响应
-            if (isset($result['candidates'][0]['content']['parts'][0]['inlineData']['data'])) {
-                // 直接从inlineData获取图片数据
-                $base64_data = $result['candidates'][0]['content']['parts'][0]['inlineData']['data'];
-                $image_type = 'jpg'; // 默认类型
-
-                // 解码base64数据
-                $image_data = base64_decode($base64_data);
-                if ($image_data === false) {
-                    return json([
-                        'code' => 1,
-                        'msg' => '图片解码失败',
-                        'data' => null
-                    ]);
-                }
+        // ========== 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
+        ]);
 
-                // 保存图片
-                $saveDir = ROOT_PATH . 'public/uploads/img2img/';
-                if (!is_dir($saveDir)) {
-                    mkdir($saveDir, 0755, true);
-                }
+        $response = curl_exec($ch);
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        $curlErr = curl_error($ch);
+        curl_close($ch);
 
-                $fileName = 'img2img-' . time() . '.' . $image_type;
-                $savePath = $saveDir . $fileName;
-                if (!file_put_contents($savePath, $image_data)) {
-                    return json([
-                        'code' => 1,
-                        'msg' => '图片保存失败',
-                        'data' => null
-                    ]);
-                }
+        // ========== 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]);
+        }
 
-                // 生成访问路径
-                $accessPath = '/uploads/img2img/' . $fileName;
+        // ========== 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)]]);
+        }
 
-                return json([
-                    'code' => 0,
-                    'msg' => '图生图已生成',
-                    'data' => [
-                        'image' => $accessPath,
-                        'model' => $model,
-                        'support_multi_images' => $supportMultiImages
-                    ]
-                ]);
-            } else if (isset($result['candidates'][0]['content']['parts'][0]['text'])) {
-                // 尝试从文本响应中提取图片
-                $text_content = $result['candidates'][0]['content']['parts'][0]['text'];
-                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
-                    ]);
-                }
+        // ========== 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];
+            }
+        }
 
-                // 保存图片
-                $saveDir = ROOT_PATH . 'public/uploads/img2img/';
-                if (!is_dir($saveDir)) {
-                    mkdir($saveDir, 0755, true);
-                }
+        if (!$base64Data) {
+            $errMsg = isset($result['error']['message']) ? $result['error']['message'] : '响应结构异常';
+            $this->json_response(['code' => 1, 'msg' => '[步骤6]未获取到图片数据。' . $errMsg . '。完整响应见data', 'data' => $result]);
+        }
 
-                $fileName = 'img2img-' . time() . '.' . $image_type;
-                $savePath = $saveDir . $fileName;
-                if (!file_put_contents($savePath, $image_data)) {
-                    return json([
-                        'code' => 1,
-                        'msg' => '图片保存失败',
-                        'data' => null
-                    ]);
-                }
+        // ========== 10. 保存图片到 uploads/ceshi/ ==========
+        $imageData = base64_decode($base64Data);
+        if ($imageData === false || strlen($imageData) < 100) {
+            $this->json_response(['code' => 1, 'msg' => '[步骤7]图片Base64解码失败', 'data' => null]);
+        }
 
-                // 生成访问路径
-                $accessPath = '/uploads/img2img/' . $fileName;
+        $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]);
+        }
 
-                return json([
-                    'code' => 0,
-                    'msg' => '图生图已生成',
-                    'data' => [
-                        'image' => $accessPath,
-                        'model' => $model,
-                        'support_multi_images' => $supportMultiImages
-                    ]
-                ]);
-            } else {
-                return json([
-                    'code' => 1,
-                    'msg' => 'API返回格式错误',
-                    '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;
             }
-        } catch (Exception $e) {
-            return json([
-                'code' => 1,
-                'msg' => 'API调用失败:' . $e->getMessage(),
-                'data' => null
-            ]);
         }
+        // 统一为相对于 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];
     }
 
+
     /**
      * 图生图本地测试
      */
@@ -779,4 +796,165 @@ class Index extends Api
 
         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 '无法保存透明图片';
+        }
+
+        // 释放资源
+        imagedestroy($source);
+        imagedestroy($transparent);
+
+        return $outputPath;
+    }
+
+    /**
+     * 处理图片背景透明化请求
+     */
+    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);
+    }
 }

+ 618 - 0
application/api/controller/Material.php

@@ -0,0 +1,618 @@
+<?php
+
+namespace app\api\controller;
+use app\common\controller\Api;
+use app\service\AIGatewayService;
+use think\Db;
+use think\Exception;
+
+class Material extends Api
+{
+    protected $noNeedLogin = ['*'];
+    protected $noNeedRight = ['*'];
+    public function index()
+    {
+        $this->success('Material');
+    }
+
+    /**
+     * 获取素材库列表接口
+     */
+    public function Material_List(){
+        $params = $this->request->param();
+        $search = input('search', '');
+        $where = [];
+        if (!empty($search)) {
+            $where['type'] = ['like', '%' . $search . '%'];
+        }
+        $res = Db::name('template_material')->where($where)->order('id desc')->select();
+        return json([
+            'code' => 0,
+            'msg'  => '',
+            'data' => $res,
+            'count' => count($res)
+        ]);
+    }
+
+    /**
+     * 模板关联素材查询
+     */
+    public function Template_Material_Relation(){
+        $params = $this->request->param();
+        $res = Db::name('template_material_relation')->alias('a')
+            ->field('a.*, b.material_url,c.canvasWidth,c.canvasHeight,c.size')
+            ->join('template_material b', 'a.material_id = b.id', 'left')
+            ->join('product_template c', 'a.template_id = c.id', 'left')
+            ->where('a.template_id',$params['id'])->select();
+
+        // 处理null值,转换为空字符串
+        if($res){
+            foreach($res as &$item){
+                foreach($item as $key => &$value){
+                    if($value === null){
+                        $value = '';
+                    }
+                }
+            }
+            return json([
+                'code' => 0,
+                'msg'  => '',
+                'data' => $res,
+                'count' => count($res)
+            ]);
+        }else{
+            return json([
+                'code' => 1,
+                'msg'  => '此模版暂无作品',
+                'data' => '',
+                'count' => 0
+            ]);
+        }
+    }
+
+    /**
+     * 新增模版(生成模版)
+     */
+    public function Template_Material_Add(){
+        $params = $this->request->param();
+//        echo "<pre>";
+//        print_r($params);
+//        echo "<pre>";die;
+        // 处理 uploaded_materials:保存素材图片到 uploads/material/ 并写入 template_material 表
+        $layerIdToMaterial = []; // layer_id => ['id'=>material_id, 'url'=>material_url]
+        if (!empty($params['uploaded_materials'])) {
+            $materialSavePath = str_replace('\\', '/', ROOT_PATH . 'public/uploads/material/' . date('Y-m-d') . '/');
+            if (!is_dir($materialSavePath)) {
+                mkdir($materialSavePath, 0755, true);
+            }
+            foreach ($params['uploaded_materials'] as $item) {
+                $base64Data = $item['data'] ?? '';
+                if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg);base64,([A-Za-z0-9+\/=]+)/i', $base64Data, $m)) {
+                    continue;
+                }
+                $imageType = strtolower($m[1]);
+                $imageData = base64_decode($m[2]);
+                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/' . date('Y-m-d') . '/' . $fileName;
+                $materialRecord = [
+                    'sys_id' => $params['sys_id'] ?? '',
+                    'material_url' => $materialUrl,
+                    'type' => $item['type'] ?? '',
+                    'create_time' => date('Y-m-d H:i:s'),
+                    'count' => 1
+                ];
+                $materialId = Db::name('template_material')->insertGetId($materialRecord);
+                if ($materialId && isset($item['layer_id'])) {
+                    $layerIdToMaterial[$item['layer_id']] = ['id' => $materialId, 'url' => $materialUrl];
+                }
+            }
+        }
+
+        $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'template' .'/'. date('Y-m-d')  . '/';
+        // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
+        $save_path = str_replace('\\', '/', $save_path);
+        // 自动创建文件夹(如果不存在)
+        if (!is_dir($save_path)) {
+            mkdir($save_path, 0755, true);
+        }
+
+        // 提取base64图片数据
+        $previewImage = $params['previewImage'];
+        // 匹配base64图片数据
+        preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $previewImage, $matches);
+        if (empty($matches)) {
+            return '未找到图片数据';
+        }
+        $image_type = $matches[1];
+        $base64_data = $matches[2];
+        // 解码base64数据
+        $image_data = base64_decode($base64_data);
+        if ($image_data === false) {
+            return '图片解码失败';
+        }
+        // 生成唯一文件名(包含正确的扩展名)
+        $file_name = uniqid() . '_' . date('YmdHis') . '.' . $image_type;
+        $full_file_path = $save_path . $file_name;
+
+//         保存图片到文件系统
+        if (!file_put_contents($full_file_path, $image_data)) {
+            return '图片保存失败';
+        }
+        // 生成数据库存储路径(使用正斜杠格式)
+        $db_img_path = '/uploads/template/'. date('Y-m-d')  .'/' . $file_name;
+
+        // 生成缩略图
+        $thumbnail_path = $this->generateThumbnail($full_file_path, $save_path, $file_name);
+        $db_thumbnail_path = '/uploads/template/'.date('Y-m-d')  .'/' . $thumbnail_path;
+
+        //新增到模版表(product_template)
+        $record['toexamine'] = '审核通过';
+
+        $record['sys_id'] = $params['sys_id'];
+        $record['canvasWidth'] = $params['canvasWidth'];
+        $record['canvasHeight'] = $params['canvasHeight'];
+        $record['size'] = $params['canvasRatio'];
+        $record['template_image_url'] = $db_img_path;//原图
+        $record['thumbnail_image'] = $db_thumbnail_path;//缩略图
+
+        $record['sys_rq'] = date('Y-m-d');
+        $record['create_time'] = date('Y-m-d H:i:s');
+
+        // 插入模板记录并获取ID
+        $templateId = Db::name('product_template')->insertGetId($record);
+
+        if (!$templateId) {
+            // 如果数据库插入失败,删除已保存的图片
+            if (file_exists($full_file_path)) {
+                unlink($full_file_path);
+            }
+            return '数据库插入失败';
+        }
+
+        // 处理layers数据,插入到模版-素材表(template_material_relation)
+        if (!empty($params['layers'])) {
+            $layers = $params['layers'];
+            foreach ($layers as $layer) {
+                $materialId = $layer['material_id'] ?? null;
+                $materialUrl = isset($layer['url']) ? $layer['url'] : '';
+                if (isset($layer['id']) && isset($layerIdToMaterial[$layer['id']])) {
+                    $materialId = $layerIdToMaterial[$layer['id']]['id'];
+                    $materialUrl = $layerIdToMaterial[$layer['id']]['url'];
+                }
+                $relationData = [
+                    'template_id' => $templateId,//模版ID
+                    'sys_id' => $params['sys_id'],//用户名
+                    'material_id' => $materialId,//素材ID
+                    'z_index' => $layer['id'],//层级
+                    'layer_name' => $layer['name'],//图层名称
+                    'layer_type' => $layer['type'],//类型
+                    'material_url' => $materialUrl,//素材图片
+                    'position_x' => $layer['x'],
+                    'position_y' => $layer['y'],
+                    'width' => $layer['width'],
+                    'height' => $layer['height'],
+                    'rotation' => $layer['rotation'],//素材旋转角度
+                    'opacity' => $layer['opacity'],//素材透明度
+                    'visible' => $layer['visible'],//图层是否显示
+                    'locked' => isset($layer['locked']) && $layer['locked'] ? 1 : 0,//图层是否锁住
+                    //文字部分参数
+                    'text_content' => isset($layer['text']) ? $layer['text'] : '',//文字内容
+                    'font_family' => isset($layer['fontFamily']) ? $layer['fontFamily'] : '',//字体(如 Arial)
+                    'font_size' => isset($layer['fontSize']) ? $layer['fontSize'] : '',//字号大小
+                    'font_color' => isset($layer['color']) ? $layer['color'] : '',//文字颜色
+                    'background_border_radius' => isset($layer['background_border_radius']) ? $layer['background_border_radius'] : '',//背景圆角
+                    'background_color' => isset($layer['backgroundColor']) ? $layer['backgroundColor'] : '',//文字背景颜色
+                    'text_align' => isset($layer['textAlign']) ? $layer['textAlign'] : '',//对齐方式
+                    'font_weight' => isset($layer['fontWeight']) ? $layer['fontWeight'] : '',//加粗
+                    'font_style' => isset($layer['fontStyle']) ? $layer['fontStyle'] : '',//斜体
+                    'font_underline' => isset($layer['textDecoration']) ? $layer['textDecoration'] : '',//下划线
+                    'line_height' => isset($layer['lineHeight']) ? $layer['lineHeight'] : '',//行高
+                    'letter_spacing' => isset($layer['letterSpacing']) ? $layer['letterSpacing'] : '',//字距
+                    //形状与线条
+                    'shape_type' => $layer['shape_type'] ?? $layer['shapeType'] ?? '',//形状类型 rect/circle/ellipse/line
+                    'fill_mode' => $layer['fill_mode'] ?? $layer['fillMode'] ?? '',//填充模式 solid/none
+                    'fill_color' => $layer['fill_color'] ?? $layer['fillColor'] ?? '',//填充色
+                    'stroke_color' => $layer['stroke_color'] ?? $layer['strokeColor'] ?? '',//描边色
+                    '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')
+
+
+                ];
+                // 插入关联记录
+                Db::name('template_material_relation')->insert($relationData);
+            }
+        }
+        return json([
+            'code' => 0,
+            'msg'  => '',
+            'data' => '',
+            'template_id' => $templateId,
+            'template_image_url' => $db_img_path,
+            'template_image' => $db_thumbnail_path
+        ]);
+    }
+
+    /**
+     * 修改模版
+     */
+    public function Template_Material_Update(){
+        $params = $this->request->param();
+//        echo "<pre>";
+//        print_r($params);
+//        echo "<pre>";die;
+        // 验证模板ID
+        if (empty($params['template_id'])) {
+            return json([
+                'code' => 1,
+                'msg'  => '模板ID不能为空',
+                'data' => ''
+            ]);
+        }
+
+        $templateId = $params['template_id'];
+
+        // 检查模板是否存在
+        $template = Db::name('product_template')->where('id', $templateId)->find();
+        if (!$template) {
+            return json([
+                'code' => 1,
+                'msg'  => '模板不存在',
+                'data' => ''
+            ]);
+        }
+
+        // 处理 uploaded_materials:修改时会有新的素材图上传,保存到 uploads/material/ 并写入 template_material 表(参考新增模版)
+        $layerIdToMaterial = [];
+        if (!empty($params['uploaded_materials'])) {
+            $materialSavePath = str_replace('\\', '/', ROOT_PATH . 'public/uploads/material/' . date('Y-m-d') . '/');
+            if (!is_dir($materialSavePath)) {
+                mkdir($materialSavePath, 0755, true);
+            }
+            foreach ($params['uploaded_materials'] as $item) {
+                $base64Data = $item['data'] ?? '';
+                if (empty($base64Data) || !preg_match('/data:image\/(png|jpg|jpeg);base64,([A-Za-z0-9+\/=]+)/i', $base64Data, $m)) {
+                    continue;
+                }
+                $imageType = strtolower($m[1]);
+                $imageData = base64_decode($m[2]);
+                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/' . date('Y-m-d') . '/' . $fileName;
+                $materialRecord = [
+                    'sys_id' => $params['sys_id'] ?? '',
+                    'material_url' => $materialUrl,
+                    'type' => $item['type'] ?? '',
+                    'create_time' => date('Y-m-d H:i:s'),
+                    'count' => 1
+                ];
+                $materialId = Db::name('template_material')->insertGetId($materialRecord);
+                if ($materialId && isset($item['layer_id'])) {
+                    $layerIdToMaterial[$item['layer_id']] = ['id' => $materialId, 'url' => $materialUrl];
+                }
+            }
+        }
+
+        // 处理图片更新
+        $db_img_path = $template['template_image_url'];
+        $db_thumbnail_path = $template['thumbnail_image'];
+
+        if (!empty($params['previewImage'])) {
+            $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'template' .'/'. date('Y-m-d')  . '/';
+            // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
+            $save_path = str_replace('\\', '/', $save_path);
+            // 自动创建文件夹(如果不存在)
+            if (!is_dir($save_path)) {
+                mkdir($save_path, 0755, true);
+            }
+
+            // 提取base64图片数据
+            $previewImage = $params['previewImage'];
+            // 匹配base64图片数据
+            preg_match('/data:image\/(png|jpg|jpeg);base64,([^"]+)/', $previewImage, $matches);
+            if (empty($matches)) {
+                return json([
+                    'code' => 1,
+                    'msg'  => '未找到图片数据',
+                    'data' => ''
+                ]);
+            }
+            $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' => ''
+                ]);
+            }
+            // 生成唯一文件名(包含正确的扩展名)
+            $file_name = uniqid() . '_' . date('YmdHis') . '.' . $image_type;
+            $full_file_path = $save_path . $file_name;
+
+            // 保存图片到文件系统
+            if (!file_put_contents($full_file_path, $image_data)) {
+                return json([
+                    'code' => 1,
+                    'msg'  => '图片保存失败',
+                    'data' => ''
+                ]);
+            }
+            // 生成数据库存储路径(使用正斜杠格式)
+            $db_img_path = '/uploads/template/'. date('Y-m-d')  .'/' . $file_name;
+
+            // 生成缩略图
+            $thumbnail_path = $this->generateThumbnail($full_file_path, $save_path, $file_name);
+            $db_thumbnail_path = '/uploads/template/'.date('Y-m-d')  .'/' . $thumbnail_path;
+
+            // 删除旧图片
+            if (!empty($template['template_image_url'])) {
+                $oldImagePath = ROOT_PATH . 'public' . $template['template_image_url'];
+                if (file_exists($oldImagePath)) {
+                    unlink($oldImagePath);
+                }
+            }
+            if (!empty($template['thumbnail_image'])) {
+                $oldThumbnailPath = ROOT_PATH . 'public' . $template['thumbnail_image'];
+                if (file_exists($oldThumbnailPath)) {
+                    unlink($oldThumbnailPath);
+                }
+            }
+        }
+
+        // 更新模版表(product_template)
+        $record['canvasWidth'] = $params['canvasWidth'];
+        $record['canvasHeight'] = $params['canvasHeight'];
+        $record['size'] = $params['canvasRatio'];
+        if (!empty($db_img_path)) {
+            $record['template_image_url'] = $db_img_path;//原图
+            $record['thumbnail_image'] = $db_thumbnail_path;//缩略图
+        }
+
+        $record['sys_rq'] = date('Y-m-d');
+        $record['template_name'] = $params['template_name'];
+        $record['update_time'] = date('Y-m-d H:i:s');
+
+        // 更新模板记录
+        $res = Db::name('product_template')->where('id', $templateId)->update($record);
+
+        if (!$res) {
+            return json([
+                'code' => 1,
+                'msg'  => '数据库更新失败',
+                'data' => ''
+            ]);
+        }
+
+        // 处理layers数据,更新模版-素材表(template_material_relation)
+        if (!empty($params['layers'])) {
+            // 删除旧的关联记录
+            Db::name('template_material_relation')->where('template_id', $templateId)->delete();
+
+            $layers = $params['layers'];
+            foreach ($layers as $layer) {
+                $materialId = $layer['material_id'] ?? null;
+                $materialUrl = isset($layer['url']) ? $layer['url'] : '';
+                if (isset($layer['id']) && isset($layerIdToMaterial[$layer['id']])) {
+                    $materialId = $layerIdToMaterial[$layer['id']]['id'];
+                    $materialUrl = $layerIdToMaterial[$layer['id']]['url'];
+                }
+                $relationData = [
+                    'template_id' => $templateId,//模版ID
+                    'sys_id' => $params['sys_id'] ?? '',//用户名
+                    'material_id' => $materialId,//素材ID
+                    'z_index' => $layer['id'],//层级
+                    'layer_name' => $layer['name'],//图层名称
+                    'layer_type' => $layer['type'],//类型
+                    'material_url' => $materialUrl,//素材图片
+                    'position_x' => $layer['x'],
+                    'position_y' => $layer['y'],
+                    'width' => $layer['width'],
+                    'height' => $layer['height'],
+                    'rotation' => $layer['rotation'],//素材旋转角度
+                    'opacity' => $layer['opacity'],//素材透明度
+                    'visible' => $layer['visible'],//图层是否显示
+                    'locked' => isset($layer['locked']) && $layer['locked'] ? 1 : 0,//图层是否锁住
+                    //文字部分参数
+                    'text_content' => isset($layer['text']) ? $layer['text'] : '',//文字内容
+                    'font_family' => isset($layer['fontFamily']) ? $layer['fontFamily'] : '',//字体(如 Arial)
+                    'font_size' => isset($layer['fontSize']) ? $layer['fontSize'] : '',//字号大小
+                    'font_color' => isset($layer['color']) ? $layer['color'] : '',//文字颜色
+                    'background_color' => isset($layer['backgroundColor']) ? $layer['backgroundColor'] : '',//文字背景颜色
+                    'background_border_radius' => isset($layer['background_border_radius']) ? $layer['background_border_radius'] : '',//背景圆角
+                    'text_align' => isset($layer['textAlign']) ? $layer['textAlign'] : '',//对齐方式
+                    'font_weight' => isset($layer['fontWeight']) ? $layer['fontWeight'] : '',//加粗
+                    'font_style' => isset($layer['fontStyle']) ? $layer['fontStyle'] : '',//斜体
+                    'font_underline' => isset($layer['textDecoration']) ? $layer['textDecoration'] : '',//下划线
+                    'line_height' => isset($layer['lineHeight']) ? $layer['lineHeight'] : '',//行高
+                    'letter_spacing' => isset($layer['letterSpacing']) ? $layer['letterSpacing'] : '',//字距
+                    //形状与线条
+                    'shape_type' => $layer['shape_type'] ?? $layer['shapeType'] ?? '',//形状类型 rect/circle/ellipse/line
+                    'fill_mode' => $layer['fill_mode'] ?? $layer['fillMode'] ?? '',//填充模式 solid/none
+                    'fill_color' => $layer['fill_color'] ?? $layer['fillColor'] ?? '',//填充色
+                    'stroke_color' => $layer['stroke_color'] ?? $layer['strokeColor'] ?? '',//描边色
+                    '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')
+
+                ];
+                // 插入关联记录
+                Db::name('template_material_relation')->insert($relationData);
+            }
+        }
+        return json([
+            'code' => 0,
+            'msg'  => '修改成功',
+            'data' => '',
+            'template_id' => $templateId,
+            'template_image_url' => $db_img_path,
+            'template_image' => $db_thumbnail_path
+        ]);
+    }
+
+    /**
+     * 生成缩略图(高质量)
+     * @param string $originalPath 原图路径
+     * @param string $savePath 保存目录
+     * @param string $fileName 原文件名
+     * @return string 缩略图文件名
+     */
+    private function generateThumbnail($originalPath, $savePath, $fileName) {
+        // 获取图片信息
+        $imageInfo = getimagesize($originalPath);
+        if (!$imageInfo) {
+            return '';
+        }
+
+        $width = $imageInfo[0];
+        $height = $imageInfo[1];
+
+        // 计算缩略图尺寸(保持比例,最大宽度400)
+        $maxWidth = 400;
+        $maxHeight = 400;
+
+        if ($width > $maxWidth || $height > $maxHeight) {
+            $ratio = min($maxWidth / $width, $maxHeight / $height);
+            $thumbWidth = round($width * $ratio);
+            $thumbHeight = round($height * $ratio);
+        } else {
+            $thumbWidth = $width;
+            $thumbHeight = $height;
+        }
+
+        // 创建缩略图画布
+        $thumbnail = imagecreatetruecolor($thumbWidth, $thumbHeight);
+
+        // 根据图片类型创建图像资源
+        switch ($imageInfo[2]) {
+            case IMAGETYPE_JPEG:
+                $source = imagecreatefromjpeg($originalPath);
+                break;
+            case IMAGETYPE_PNG:
+                $source = imagecreatefrompng($originalPath);
+                // 处理PNG透明
+                imagealphablending($thumbnail, false);
+                imagesavealpha($thumbnail, true);
+                $transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
+                imagefilledrectangle($thumbnail, 0, 0, $thumbWidth, $thumbHeight, $transparent);
+                break;
+            case IMAGETYPE_GIF:
+                $source = imagecreatefromgif($originalPath);
+                break;
+            default:
+                return '';
+        }
+
+        if (!$source) {
+            return '';
+        }
+
+        // 调整图片大小(使用高质量缩放)
+        imagecopyresampled($thumbnail, $source, 0, 0, 0, 0, $thumbWidth, $thumbHeight, $width, $height);
+
+        // 生成缩略图文件名
+        $pathInfo = pathinfo($fileName);
+        $thumbnailName = $pathInfo['filename'] . '_thumb.' . $pathInfo['extension'];
+        $thumbnailPath = $savePath . $thumbnailName;
+
+        // 保存缩略图(使用高质量设置)
+        switch ($imageInfo[2]) {
+            case IMAGETYPE_JPEG:
+                imagejpeg($thumbnail, $thumbnailPath, 95); // 95% 质量,接近原图
+                break;
+            case IMAGETYPE_PNG:
+                imagepng($thumbnail, $thumbnailPath, 3); // 压缩级别 3,保持较高质量
+                break;
+            case IMAGETYPE_GIF:
+                imagegif($thumbnail, $thumbnailPath);
+                break;
+        }
+
+        // 释放资源
+        imagedestroy($source);
+        imagedestroy($thumbnail);
+
+        return $thumbnailName;
+    }
+
+    /**
+     * 发布模版(release=1)
+     */
+    public function Template_Material_Publish(){
+        $params = $this->request->param();
+
+        $record['release'] = 1;
+        $record['update_time'] = date('Y-m-d H:i:s');
+        $res = Db::name('product_template')->where('id', $params['template_id'])->update($record);
+        if (!$res) {
+            return json([
+                'code' => 1,
+                'msg'  => '发布失败',
+                'data' => ''
+            ]);
+        }
+        return json([
+            'code' => 0,
+            'msg'  => '发布成功'
+        ]);
+    }
+
+    /**
+     * 取消发布模版(release=0)
+     */
+    public function Template_Material_Unpublish(){
+        $params = $this->request->param();
+        $record['release'] = 0;
+        $record['update_time'] = date('Y-m-d H:i:s');
+        $res = Db::name('product_template')->where('id', $params['template_id'])->update($record);
+        if (!$res) {
+            return json([
+                'code' => 1,
+                'msg'  => '发布失败',
+                'data' => ''
+            ]);
+        }
+        return json([
+            'code' => 0,
+            'msg'  => '发布成功'
+        ]);
+    }
+
+    /**
+     * 删除模版
+     */
+    public function Template_Material_Delete(){
+        $params = $this->request->param();
+        $record['mod_rq'] = date('Y-m-d H:i:s');
+        $res = Db::name('product_template')->where('id', $params['template_id'])->update($record);
+        if (!$res) {
+            return json([
+                'code' => 1,
+                'msg'  => '模版删除失败',
+                'data' => ''
+            ]);
+        }
+        return json([
+            'code' => 0,
+            'msg'  => '模版删除成功'
+        ]);
+    }
+
+
+
+}

+ 29 - 7
application/api/controller/Product.php

@@ -287,6 +287,7 @@ class Product extends Api
 
         $product_image = \db('product_image')
             ->where('product_id', $productId)
+            ->whereNull('mod_rq')
             ->order('id desc')
             ->select();
 
@@ -328,32 +329,53 @@ class Product extends Api
     public function productAdd()
     {
 
-        // 1. 请求方法验证(可提取为公共方法)
+        // 请求方法验证(可提取为公共方法)
         if (!$this->request->isPost()) {
             throw new \Exception('非法请求');
         }
 
-        // 2. 获取并验证参数
+        // 获取并验证参数
         $param = $this->request->post();
         $this->validateProductParams($param, ['product_name', 'product_code']);
 
-        // 3. 准备数据(使用助手函数处理时间)
+        //处理产品图片 base64 -> 保存到 uploads/merchant/{product_code前段}/{product_code}/{product_name}.png
+        $productImgPath = '';
+        if (!empty($param['product_img'])) {
+            $base64Data = $param['product_img'];
+            if (preg_match('/data:image\/(png|jpg|jpeg);base64,([A-Za-z0-9+\/=]+)/i', $base64Data, $m)) {
+                $imageType = strtolower($m[1]);
+                $imageData = base64_decode($m[2]);
+                if ($imageData !== false && strlen($imageData) >= 100) {
+                    $productCode = $param['product_code'];
+                    $prefix = strlen($productCode) >= 4 ? substr($productCode, 0, -4) : $productCode; // 后四位前面的数值
+                    $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 . '/');
+                    if (!is_dir($saveDir)) {
+                        mkdir($saveDir, 0755, true);
+                    }
+                    if (file_put_contents($saveDir . $fileName, $imageData)) {
+                        $productImgPath = 'uploads/merchant/' . $prefix . '/' . $productCode . '/' . $fileName;
+                    }
+                }
+            }
+        }
+
         $data = [
             'product_name' => $param['product_name'],
             'product_code' => $param['product_code'],
             'createTime'   => date('Y-m-d H:i:s', time()),
             'create_name'  => isset($param['create_name']) ? $param['create_name'] : '',
             'merchant_id'  => isset($param['merchant_id']) ? intval($param['merchant_id']) : 0,
-            'product_img'  => isset($param['product_img']) ? $param['product_img'] : '',
+            'product_img'  => $productImgPath,
         ];
 
-        // 4. 验证产品编码唯一性
+        //验证产品编码唯一性
         if (Db::name('product')->where('product_code', $data['product_code'])->count() > 0) {
             $this->error('产品编码已存在');
         }
 
-        // 5. 使用事务确保数据一致性
-
         $result = Db::name('product')->insert($data);
 
         if ($result) {

+ 220 - 151
application/api/controller/WorkOrder.php

@@ -17,6 +17,46 @@ class WorkOrder extends Api{
     protected $noNeedLogin = ['*'];
     protected $noNeedRight = ['*'];
     public function index(){echo '访问成功';}
+
+    /**
+     * 获取图片生成状态
+     * @return json 任务状态和图片路径
+     */
+    public function GetImageStatus(){
+        $params = $this->request->param();
+        $taskId = $params['task_id'];
+        if (empty($taskId)) {
+            $res = [
+                'code' => 1,
+                'msg' => '任务ID不能为空'
+            ];
+            return json($res);
+        }
+        // 从Redis中获取任务状态(支持文生图 text_to_image_task 和图生图 img_to_img_task)
+        $redis = new \Redis();
+        $redis->connect('127.0.0.1', 6379);
+        $redis->auth('123456');
+        $redis->select(15);
+        $taskData = $redis->get("img_to_img_task:{$taskId}");
+        if (!$taskData) {
+            $taskData = $redis->get("text_to_image_task:{$taskId}");
+        }
+        if (!$taskData) {
+            $res = [
+                'code' => 1,
+                'msg' => '任务不存在或已过期',
+            ];
+            return json($res);
+        }
+        $taskInfo = json_decode($taskData, true);
+        $res = [
+            'code' => 0,
+            'msg' => '查询成功',
+            'data' => $taskInfo
+        ];
+        return json($res);
+    }
+
     /**
      * AI队列入口处理  出图接口
      * 此方法处理图像转换为文本的请求,将图像信息存入队列以供后续处理。
@@ -74,12 +114,37 @@ class WorkOrder extends Api{
             return json($res);
 
         }elseif($params['status_val'] == '文生图'){
-            $service->handleTextToImg($params);
-            $res = [
-                'code' => 0,
-                'msg' => '正在生成图片中,请稍等.....'
-            ];
+            $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('请求失败');
         }
@@ -324,93 +389,93 @@ class WorkOrder extends Api{
      * 第二步:使用九个分镜头图进行裁剪单图生成连贯视频
      * 第三步:在通过分镜头视频拼接成一个完整的视频(列如每个分镜头视频为8秒,九个为72秒形成完整视频)
      */
-//    public function Get_txttonineimg()
-//    {
-//        // 发起接口请求
-////        $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';
-//
-////        $params = $this->request->param();
-//        $prompt = '生成一个苹果(九个分镜头图片)';
-//
-//        $requestData = [
-//            "contents" => [
-//                [
-//                    "role" => "user",
-//                    "parts" => [
-//                        ["text" => $prompt]
-//                    ]
-//                ]
-//            ],
-//            "generationConfig" => [
-//                "responseModalities" => ["TEXT", "IMAGE"],
-//                "imageConfig" => [
-//                    "aspectRatio" => "1:1"
-//                ]
-//            ]
-//        ];
-//
-//        $ch = curl_init();
-//        curl_setopt($ch, CURLOPT_URL, $apiUrl);
-//        curl_setopt($ch, CURLOPT_POST, true);
-//        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData, JSON_UNESCAPED_UNICODE));
-//        curl_setopt($ch, CURLOPT_HTTPHEADER, [
-//            'Content-Type: application/json',
-//            'Authorization: Bearer ' . $apiKey
-//        ]);
-//        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
-//        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 开发环境临时关闭SSL验证
-//        curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 生成图片超时时间(建议60秒)
-//
-//        $response = curl_exec($ch);
-//        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-//
-//        $error = curl_error($ch);
-//        curl_close($ch);
-//        $res = json_decode($response, true);
-//
-//        // 构建URL路径(使用正斜杠)
-//        $url_path = '/uploads/txtnewimg/';
-//        // 构建物理路径(使用正斜杠确保统一格式)
-//        $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'txtnewimg'  . '/';
-//        // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
-//        $save_path = str_replace('\\', '/', $save_path);
-//        // 自动创建文件夹(如果不存在)
-//        if (!is_dir($save_path)) {
-//            mkdir($save_path, 0755, true);
-//        }
-//
-//        // 提取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 '未找到图片数据';
-//        }
-//        $image_type = $matches[1];
-//        $base64_data = $matches[2];
-//
-//        // 解码base64数据
-//        $image_data = base64_decode($base64_data);
-//        if ($image_data === false) {
-//            return '图片解码失败';
-//        }
-//
-//        // 生成唯一文件名(包含扩展名)
-//        $file_name = uniqid() . '.' . $image_type;
-//        $full_file_path = $save_path . $file_name;
-//
-//        // 保存图片到文件系统
-//        if (!file_put_contents($full_file_path, $image_data)) {
-//            return '图片保存失败';
-//        }
-//        // 生成数据库存储路径(使用正斜杠格式)
-//        $db_img_path = $url_path . $file_name;
-//        return  $db_img_path;
-//    }
+    public function Get_txttonineimg()
+    {
+        // 发起接口请求
+//        $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';
+
+//        $params = $this->request->param();
+        $prompt = '生成一个苹果(九个分镜头图片)';
+
+        $requestData = [
+            "contents" => [
+                [
+                    "role" => "user",
+                    "parts" => [
+                        ["text" => $prompt]
+                    ]
+                ]
+            ],
+            "generationConfig" => [
+                "responseModalities" => ["TEXT", "IMAGE"],
+                "imageConfig" => [
+                    "aspectRatio" => "1:1"
+                ]
+            ]
+        ];
+
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $apiUrl);
+        curl_setopt($ch, CURLOPT_POST, true);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData, JSON_UNESCAPED_UNICODE));
+        curl_setopt($ch, CURLOPT_HTTPHEADER, [
+            'Content-Type: application/json',
+            'Authorization: Bearer ' . $apiKey
+        ]);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 开发环境临时关闭SSL验证
+        curl_setopt($ch, CURLOPT_TIMEOUT, 60); // 生成图片超时时间(建议60秒)
+
+        $response = curl_exec($ch);
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+        $error = curl_error($ch);
+        curl_close($ch);
+        $res = json_decode($response, true);
+
+        // 构建URL路径(使用正斜杠)
+        $url_path = '/uploads/txtnewimg/';
+        // 构建物理路径(使用正斜杠确保统一格式)
+        $save_path = ROOT_PATH . 'public' . '/' . 'uploads' . '/' . 'txtnewimg'  . '/';
+        // 移除ROOT_PATH中可能存在的反斜杠,确保统一使用正斜杠
+        $save_path = str_replace('\\', '/', $save_path);
+        // 自动创建文件夹(如果不存在)
+        if (!is_dir($save_path)) {
+            mkdir($save_path, 0755, true);
+        }
+
+        // 提取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 '未找到图片数据';
+        }
+        $image_type = $matches[1];
+        $base64_data = $matches[2];
+
+        // 解码base64数据
+        $image_data = base64_decode($base64_data);
+        if ($image_data === false) {
+            return '图片解码失败';
+        }
+
+        // 生成唯一文件名(包含扩展名)
+        $file_name = uniqid() . '.' . $image_type;
+        $full_file_path = $save_path . $file_name;
+
+        // 保存图片到文件系统
+        if (!file_put_contents($full_file_path, $image_data)) {
+            return '图片保存失败';
+        }
+        // 生成数据库存储路径(使用正斜杠格式)
+        $db_img_path = $url_path . $file_name;
+        return  $db_img_path;
+    }
 
 
 
@@ -421,46 +486,46 @@ class WorkOrder extends Api{
         $model = $params['model'];//模型
         $size = $params['size'];//尺寸
 
-         // 调用AI生成图片
-         $aiGateway = new AIGatewayService();
-         $res = $aiGateway->callDalleApi($prompt, $model, $size);
-
-         // 提取base64图片数据
-         $imageData = '';
-         $imageType = 'png'; // 默认图片类型
-         if (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
-             $text_content = $res['candidates'][0]['content']['parts'][0]['text'];
-             // 匹配base64图片数据和类型
-             if (preg_match('/data:image\/([a-zA-Z0-9]+);base64,([^\s]+)/', $text_content, $matches)) {
-                 $imageType = strtolower($matches[1]);
-                 $base64Data = $matches[2];
-                 // 解码base64数据
-                 $imageData = base64_decode($base64Data);
-             }
-         }
-
-         if (!$imageData) {
-             return json(['code' => 1, 'msg' => '图片生成失败,未找到有效图片数据']);
-         }
-
-         // 创建保存目录(public/uploads/log/YYYY-MM/)
-         $yearMonth = date('Ym');
-         $saveDir = ROOT_PATH . 'public/uploads/ceshi/' . $yearMonth . '/';
-         if (!is_dir($saveDir)) {
-             mkdir($saveDir, 0755, true);
-         }
-
-         // 生成唯一文件名
-         $fileName = uniqid() . '.' . $imageType;
-         $filePath = $saveDir . $fileName;
-
-         // 保存图片到本地文件
-         if (file_put_contents($filePath, $imageData) === false) {
-             return json(['code' => 1, 'msg' => '图片保存失败']);
-         }
-
-         // 生成前端可访问的URL
-         $imageUrl = '/uploads/ceshi/' . $yearMonth . '/' . $fileName;
+        // 调用AI生成图片
+        $aiGateway = new AIGatewayService();
+        $res = $aiGateway->callDalleApi($prompt, $model, $size);
+
+        // 提取base64图片数据
+        $imageData = '';
+        $imageType = 'png'; // 默认图片类型
+        if (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
+            $text_content = $res['candidates'][0]['content']['parts'][0]['text'];
+            // 匹配base64图片数据和类型
+            if (preg_match('/data:image\/([a-zA-Z0-9]+);base64,([^\s]+)/', $text_content, $matches)) {
+                $imageType = strtolower($matches[1]);
+                $base64Data = $matches[2];
+                // 解码base64数据
+                $imageData = base64_decode($base64Data);
+            }
+        }
+
+        if (!$imageData) {
+            return json(['code' => 1, 'msg' => '图片生成失败,未找到有效图片数据']);
+        }
+
+        // 创建保存目录(public/uploads/log/YYYY-MM/)
+        $yearMonth = date('Ym');
+        $saveDir = ROOT_PATH . 'public/uploads/ceshi/' . $yearMonth . '/';
+        if (!is_dir($saveDir)) {
+            mkdir($saveDir, 0755, true);
+        }
+
+        // 生成唯一文件名
+        $fileName = uniqid() . '.' . $imageType;
+        $filePath = $saveDir . $fileName;
+
+        // 保存图片到本地文件
+        if (file_put_contents($filePath, $imageData) === false) {
+            return json(['code' => 1, 'msg' => '图片保存失败']);
+        }
+
+        // 生成前端可访问的URL
+        $imageUrl = '/uploads/ceshi/' . $yearMonth . '/' . $fileName;
 
         // 返回标准JSON响应
         return json([
@@ -1163,7 +1228,6 @@ class WorkOrder extends Api{
                 '失败'     => $statusCount[-1] ?? 0,
                 '当前状态' => $statusText
             ]
-
         ]);
     }
 
@@ -1178,7 +1242,7 @@ class WorkOrder extends Api{
         }
         $redis = new \Redis();
         $redis->connect('127.0.0.1', 6379);
-        $redis->auth('123456');
+        $redis->auth('');
         $redis->select(15);
         return $redis;
     }
@@ -1377,25 +1441,30 @@ class WorkOrder extends Api{
         // 构建查询条件
         $where = [];
         if (!empty($params['search'])) {
-            $where['chinese_description'] = ['like', '%' . $params['search'] . '%'];
+            $where['chinese_description|template_name|style'] = ['like', '%' . $params['search'] . '%'];
         }
-        // 查询模版表-style分类名称
-        $products = Db::name('product_template')->order('id desc')->where($where)
-            ->where('type','文生图')
-            ->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'];
-                }
-            }
+        if (!empty($params['sys_id'])) {
+            $where['sys_id'] = ['like', '%' . $params['sys_id'] . '%'];
+            $products = Db::name('product_template')->order('id desc')->where($where)
+                ->whereNull('mod_rq')
+                ->select();
+        }else{
+            $products = Db::name('product_template')->order('id desc')->where($where)
+                ->where('release','1')
+                ->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' => '请求成功',

+ 41 - 0
application/error/controller/Index.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace app\error\controller;
+
+use think\Controller;
+use think\Request;
+
+/**
+ * 错误模块 - 解决「模块不存在:error」
+ * 当请求被错误路由到 error 模块时,返回 JSON 提示正确访问方式
+ */
+class Index extends Controller
+{
+    public function index()
+    {
+        return $this->jsonError();
+    }
+
+    public function _500()
+    {
+        return $this->jsonError();
+    }
+
+    private function jsonError()
+    {
+        $base = rtrim(request()->domain() . request()->root(), '/');
+        return json([
+            'code' => 404,
+            'msg' => '接口地址错误,请使用下列任一地址',
+            'data' => [
+                'urls' => [
+                    $base . '/img2img',
+                    $base . '/api/img2img',
+                    $base . '/api/Index/img_to_img'
+                ],
+                'method' => 'POST',
+                'params' => ['product_img', 'template_img', 'prompt']
+            ]
+        ]);
+    }
+}

+ 11 - 2
application/job/ImageArrJob.php

@@ -25,11 +25,20 @@ class ImageArrJob
         $data = (array)$data;
 
         if (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";
+
         } else {
 
             $task_id = $data['task_id'];

+ 2 - 4
application/job/ImageJob.php

@@ -32,7 +32,7 @@ class ImageJob{
 
             // 设置锁键名
             $lockKey = "image_to_text_lock:{$taskId}";
-            $lockExpire = 300; // 锁的过期时间,单位:秒(5分钟)
+            $lockExpire = 180; // 锁的过期时间,单位:秒(2分钟)
 
             // 尝试获取锁(使用SET命令的NX和EX选项进行原子操作)
             // NX: 只在键不存在时设置
@@ -242,9 +242,7 @@ class ImageJob{
         $gptRes = $ai->callGptApi($imgtotxt_selectedOption,$prompt,$imageUrl,'');
 
         $gptText = trim($gptRes['choices'][0]['message']['content'] ?? '');
-        echo "<pre>";
-        print_r($gptText);
-        echo "<pre>";die;
+    
         //图生文返回内容日志runtime/logs/img_to_txt.txt
         // file_put_contents(
         //     $logDir . 'img_to_txt.txt',

+ 234 - 90
application/job/ImageToImageJob.php

@@ -4,116 +4,151 @@ use app\service\AIGatewayService;
 use think\Db;
 use think\queue\Job;
 use think\Queue;
-
+//图生图
 class ImageToImageJob{
-
     public function fire(Job $job, $data)
     {
-
-        $logId = $data['log_id'] ?? null;
-
-        try {
-            // 任务类型校验(必须是图生图)
-            if (!isset($data['type']) || $data['type'] !== '图生图') {
+        //产品图+模板图) 时走此分支
+        if (isset($data['status_val']) && $data['status_val'] == '图生图' && !empty($data['product_img']) && !empty($data['template_img'])) {
+            try {
+
+                // 获取任务ID
+                $taskId = $data['task_id'];
+                // 获取产品ID
+                $Id = $data['id'];
+
+                echo "━━━━━━━━━━ ▶ 图生图任务开始处理━━━━━━━━━━\n";
+                echo "开始时间:" . date('Y-m-d H:i:s') . "\n";
+
+                $result = $this->get_img_to_img($data);
+                $redis = new \Redis();
+                $redis->connect('127.0.0.1', 6379);
+                $redis->auth('123456');
+                $redis->select(15);
+                $redis->set("text_to_image_task:{$taskId}", json_encode([
+                    'status' => 'completed',
+                    // 'image_url' => "/uploads/merchant/690377511/6903775111138/newimg/698550113c2b8.jpeg",
+                    'image_url' => $result,
+                    'completed_at' => date('Y-m-d H:i:s')
+                ]), ['EX' => 300]); // 5分钟过期
+
+                echo "🎉 任务 {$taskId} 执行完成,图片生成成功!\n";
+                echo "结束时间:" . date('Y-m-d H:i:s') . "\n";
                 $job->delete();
-                return;
-            }
 
-            $startTime = date('Y-m-d H:i:s');
-            echo "━━━━━━━━━━ ▶ 图生图任务开始处理━━━━━━━━━━\n";
-            echo "处理时间:{$startTime}\n";
-
-            // 更新日志状态:处理中
-            if ($logId) {
-                Db::name('image_task_log')->where('id', $logId)->update([
-                    'status' => 1,
-                    'log' => '图生图处理中',
-                    'update_time' => $startTime
-                ]);
+            } catch (\Exception $e) {
+                echo "图生图失败: " . $e->getMessage() . "\n";
+                $job->delete();
             }
-
-            //拼接原图文件路径 + 图片名称
-            $old_image_url = rtrim($data['sourceDir'], '/') . '/' . ltrim($data['file_name'], '/');
-
-            $list = Db::name("text_to_image")
-                ->where('old_image_url', $old_image_url)
-                ->where('img_name', '<>', '')
-                // ->where('status', 1)
-                ->select();
-
-            if (!empty($list)) {
-                $total = count($list);
-                echo "📊 共需处理:{$total} 条记录\n\n";
-
-                foreach ($list as $index => $row) {
-                    $currentIndex = $index + 1;
-                    $begin = date('Y-m-d H:i:s');
-                    echo "处理时间:{$begin}\n";
-                    echo "👉 正在处理第 {$currentIndex} 条,ID: {$row['id']}\n";
-
-                    // 调用生成图像方法
-                    $result = $this->ImageToImage(
-                        $data["file_name"],
-                        $data["outputDir"],
-                        $row["new_image_url"],
-                        $row["img_name"],
-                        1024,
-                        1303
-                    );
-
-                    $resultText = ($result === true || $result === 1 || $result === '成功') ? '成功' : '失败或无返回';
-                    echo "✅ 处理结果:{$resultText}\n";
-
-                    $end = date('Y-m-d H:i:s');
-                    echo "完成时间:{$end}\n";
-                    echo "Processed: " . static::class . "\n";
-                    echo "图生图已处理完成\n\n";
+            $job->delete();
+        } else {
+            $logId = $data['log_id'] ?? null;
+
+            try {
+                // 任务类型校验(必须是图生图)
+                if (!isset($data['type']) || $data['type'] !== '图生图') {
+                    $job->delete();
+                    return;
                 }
 
-                // 更新日志状态:成功
+                $startTime = date('Y-m-d H:i:s');
+                echo "━━━━━━━━━━ ▶ 图生图任务开始处理━━━━━━━━━━\n";
+                echo "处理时间:{$startTime}\n";
+
+                // 更新日志状态:处理中
                 if ($logId) {
                     Db::name('image_task_log')->where('id', $logId)->update([
-                        'status' => 2,
-                        'log' => '图生图处理成功',
-                        'update_time' => date('Y-m-d H:i:s')
+                        'status' => 1,
+                        'log' => '图生图处理中',
+                        'update_time' => $startTime
                     ]);
                 }
 
-                echo date('Y-m-d H:i:s') . " 图生图任务全部完成\n";
-            } else {
-
-                echo "未找到可处理的数据,跳过执行\n";
-                if ($logId) {
-                    Db::name('image_task_log')->where('id', $logId)->update([
-                        'status' => 2,
-                        'log' => '无数据可处理,已跳过'.$old_image_url,
-                        'update_time' => date('Y-m-d H:i:s')
-                    ]);
+                //拼接原图文件路径 + 图片名称
+                $old_image_url = rtrim($data['sourceDir'], '/') . '/' . ltrim($data['file_name'], '/');
+
+                $list = Db::name("text_to_image")
+                    ->where('old_image_url', $old_image_url)
+                    ->where('img_name', '<>', '')
+                    // ->where('status', 1)
+                    ->select();
+
+                if (!empty($list)) {
+                    $total = count($list);
+                    echo "📊 共需处理:{$total} 条记录\n\n";
+
+                    foreach ($list as $index => $row) {
+                        $currentIndex = $index + 1;
+                        $begin = date('Y-m-d H:i:s');
+                        echo "处理时间:{$begin}\n";
+                        echo "👉 正在处理第 {$currentIndex} 条,ID: {$row['id']}\n";
+
+                        // 调用生成图像方法
+                        $result = $this->ImageToImage(
+                            $data["file_name"],
+                            $data["outputDir"],
+                            $row["new_image_url"],
+                            $row["img_name"],
+                            1024,
+                            1303
+                        );
+
+                        $resultText = ($result === true || $result === 1 || $result === '成功') ? '成功' : '失败或无返回';
+                        echo "✅ 处理结果:{$resultText}\n";
+
+                        $end = date('Y-m-d H:i:s');
+                        echo "完成时间:{$end}\n";
+                        echo "Processed: " . static::class . "\n";
+                        echo "图生图已处理完成\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 date('Y-m-d H:i:s') . " 图生图任务全部完成\n";
+                } else {
+
+                    echo "未找到可处理的数据,跳过执行\n";
+                    if ($logId) {
+                        Db::name('image_task_log')->where('id', $logId)->update([
+                            'status' => 2,
+                            'log' => '无数据可处理,已跳过'.$old_image_url,
+                            'update_time' => date('Y-m-d H:i:s')
+                        ]);
+                    }
                 }
-            }
 
-            // 如果还有链式任务,继续推送
-            if (!empty($data['chain_next'])) {
-                $nextType = array_shift($data['chain_next']);
-                $data['type'] = $nextType;
+                // 如果还有链式任务,继续推送
+                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');
-            }
+                    Queue::push('app\job\ImageArrJob', [
+                        'task_id' => $data['task_id'],
+                        'data' => [$data]
+                    ], 'arrimage');
+                }
 
-            $job->delete();
+                $job->delete();
 
-        }  catch (\Exception $e) {
-            //异常处理,记录失败日志
-            echo "错误信息: " . $e->getMessage() . "\n";
-            echo "文件: " . $e->getFile() . "\n";
-            echo "行号: " . $e->getLine() . "\n";
+            }  catch (\Exception $e) {
+                //异常处理,记录失败日志
+                echo "错误信息: " . $e->getMessage() . "\n";
+                echo "文件: " . $e->getFile() . "\n";
+                echo "行号: " . $e->getLine() . "\n";
 
-            // 删除当前任务
-            $job->delete();
+                // 删除当前任务
+                $job->delete();
+            }
         }
+
+
     }
 
     /**
@@ -124,6 +159,115 @@ class ImageToImageJob{
         echo "ImageJob failed: " . json_encode($data);
     }
 
+    /**
+     * Gemini 图生图:产品图 + 模板图 + 提示词 → 生成新图
+     */
+    public function get_img_to_img($data)
+    {
+
+        $prompt = trim($data['prompt'] ?? '');
+        $size = trim($data['$size'] ?? '');
+        $product_img = trim($data['product_img'] ?? '');
+        $template_img = trim($data['template_img'] ?? '');
+        $model = trim($data['model'] ?? 'gemini-3-pro-image-preview');
+
+        $defaultPrompt = '请完成产品模板替换:
+                            1. 从产品图提取产品主体、品牌名称、核心文案;
+                            2. 从模板图继承版式布局、文字排版、色彩风格、背景元素;
+                            3. 将模板图中的产品和文字替换为产品图的内容;
+                            4. 最终生成的图片与模板图视觉风格100%统一,仅替换产品和文字。';
+        $promptContent = $prompt ?: $defaultPrompt;
+
+        $aiGateway = new AIGatewayService();
+
+        // 获取图片的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'];
+        } elseif (isset($res['candidates'][0]['content']['parts'][0]['text'])) {
+            $text = $res['candidates'][0]['content']['parts'][0]['text'];
+            // text 格式多为 ![image](data:image/png;base64,XXX),支持换行
+            if (preg_match('/data:image\/(png|jpg|jpeg|webp);base64,([^\)]+)/i', $text, $m)) {
+                $base64Data = preg_replace('/\s+/', '', $m[2]);
+            }
+        }
+        if (!$base64Data) {
+            $errMsg = isset($res['error']['message']) ? $res['error']['message'] : '未获取到图片数据';
+            return ['code' => 1, 'msg' => $errMsg];
+        }
+        $imageData = base64_decode($base64Data);
+
+        if ($imageData === false || strlen($imageData) < 100) {
+            return ['code' => 1, 'msg' => '图片Base64解码失败'];
+        }
+
+        $rootPath = str_replace('\\', '/', ROOT_PATH);
+        $saveDir = rtrim($rootPath, '/') . '/public/uploads/template/';
+
+        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' => '图片保存失败'];
+        }
+
+        $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 = new \Redis();
+                $redis->connect('127.0.0.1', 6379);
+                $redis->auth('123456');
+                $redis->select(15);
+                $redis->set("img_to_img_task:" . $data['task_id'], json_encode([
+                    'status' => 'completed',
+                    'image_url' => $db_img_path,
+                    'completed_at' => date('Y-m-d H:i:s')
+                ], JSON_UNESCAPED_UNICODE), ['EX' => 300]);
+            } catch (\Exception $e) {
+                // 忽略 Redis 错误
+            }
+        }
+
+        return "成功";
+    }
+
 
     public function ImageToImage($fileName, $outputDirRaw, $new_image_url, $width, $height)
     {

+ 3 - 0
application/route.php

@@ -20,6 +20,9 @@ return [
     ],
     // 路由规则
     'user/diagrams_list/id/:id.html' => 'index/user/diagrams_list',
+    // 当请求被误解析为 error 时,返回友好提示
+    'error/500' => 'error/Index/index',
+    'error/:code' => 'error/Index/index',
 //        域名绑定到模块
 //        '__domain__'  => [
 //            'admin' => 'admin',

+ 58 - 18
application/service/AIGatewayService.php

@@ -8,7 +8,7 @@ class AIGatewayService{
      * - api_key:API 调用密钥(Token)
      * - api_url:对应功能的服务端地址
      */
-     protected $config = [
+    public $config = [
         //图生文 gemini-3-pro-preview
         'gemini_imgtotxt' => [
             'api_key' => 'sk-R4O93k4FrJTXMLYZ2eB32WDPHWiDNbeUdlUcsLjgjeDKuzFI',
@@ -34,15 +34,19 @@ class AIGatewayService{
              'api_key' => 'sk-8nTt32NDI6q7klryBehwjEfnGaGrX8m1zI0C4ddfudLtanqP',
              'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:streamGenerateContent'
          ],
-         //文生图 MID_JOURNEY
-         'submitimage' => [
-             'api_key' => 'sk-iURfrAgzAjhZ4PpPLwzmWIAhM7zKfrkwDvyxk4RVBQ4ouJNK',
-             'api_url' => 'https://chatapi.onechats.ai/mj/submit/imagine'
+        //图生图【gemini-3-pro-image-preview】
+         'gemini_imgtoimg' => [
+             'api_key' => 'sk-8nTt32NDI6q7klryBehwjEfnGaGrX8m1zI0C4ddfudLtanqP',
+             'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:streamGenerateContent'
+         ],
+         //即梦AI 创建视频任务接口【首帧图 + 尾帧图 = 新效果视频】
+         'Create_ImgToVideo' => [
+             'api_key' => '',
+             'api_url' => 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks'
          ]
      ];
 
 
-
     /**
      * 图生文
      * @param string $prompt   对图像的提问内容或提示文本
@@ -255,18 +259,54 @@ class AIGatewayService{
         }
     }
 
+    /**
+     * 图生图(产品图 + 模板图 → 新图)
+     * @param string $prompt 提示词
+     * @param string $model 模型名,如 gemini-3-pro-image-preview
+     * @param string $productImgRaw 产品图路径,如 /uploads/merchant/xxx/xxx.png
+     * @param string $templateImgRaw 模板图路径,如 /uploads/template/xxx.png
+     * @return array API 响应,成功时含 candidates[0].content.parts[0].inlineData
+     */
+    public function GeminiImToImgCallApi($promptContent, $model,$size, $product_base64Data,$product_mimeType,$template_base64Data,$template_mimeType)
+    {
+        $data = [
+            'contents' => [
+                [
+                    'role' => 'user',
+                    'parts' => [
+                        ['text' => $promptContent],
+                        ['inlineData' => ['mimeType' => $product_mimeType, 'data' => $product_base64Data]],
+                        ['inlineData' => ['mimeType' => $template_mimeType, 'data' => $template_base64Data]]
+                    ]
+                ]
+            ],
+            'generationConfig' => [
+                'responseModalities' => ['IMAGE'],
+                'imageConfig' => [
+                    'aspectRatio' => '5:4',
+                    'quality' => 'HIGH',
+                    'width' => 1000,
+                    'height' => 800
+                ],
+                'temperature' => 0.3,
+                'topP' => 0.8,
+                'maxOutputTokens' => 2048
+            ]
+        ];
+        return $this->callApi($this->config['gemini_imgtoimg']['api_url'], $this->config['gemini_imgtoimg']['api_key'], $data, 300);
+    }
 
         /**
-         * 计算最大公约数
-         */
-        public function gcd($a, $b) {
-            while ($b != 0) {
-                $temp = $a % $b;
-                $a = $b;
-                $b = $temp;
-            }
-            return $a;
+     * 计算最大公约数
+     */
+    public function gcd($a, $b) {
+        while ($b != 0) {
+            $temp = $a % $b;
+            $a = $b;
+            $b = $temp;
         }
+        return $a;
+    }
 
     /**
      * 图生图
@@ -518,7 +558,7 @@ class AIGatewayService{
      * @return array 接口响应数据(成功时返回解析后的数组)
      * @throws \Exception 接口请求失败时抛出异常
      */
-    public static function  callApi($url, $apiKey, $data)
+    public static function callApi($url, $apiKey, $data, $timeout = 60)
     {
         $maxRetries = 0;  // 减少重试次数为0,避免不必要的等待
         $attempt = 0;
@@ -533,12 +573,12 @@ class AIGatewayService{
                     CURLOPT_URL => $url,
                     CURLOPT_RETURNTRANSFER => true,
                     CURLOPT_POST => true,
-                    CURLOPT_POSTFIELDS => json_encode($data),
+                    CURLOPT_POSTFIELDS => json_encode($data, JSON_UNESCAPED_UNICODE),
                     CURLOPT_HTTPHEADER => [
                         'Content-Type: application/json',
                         'Authorization: Bearer ' . $apiKey
                     ],
-                    CURLOPT_TIMEOUT => 60,  // 减少总超时时间为60秒
+                    CURLOPT_TIMEOUT => (int) $timeout,
                     CURLOPT_SSL_VERIFYPEER => false,
                     CURLOPT_SSL_VERIFYHOST => false,
                     CURLOPT_CONNECTTIMEOUT => 15,  // 减少连接超时时间为15秒

+ 26 - 2
application/service/ImageService.php

@@ -47,7 +47,7 @@ class ImageService{
     }
 
     /**
-     * 直接调用文生图API并返回结果
+     * 文生图API并返回结果
      * @param array $params 请求参数,包含文生文提示词、模型类型等
      * @return array GPT生成的结果
      */
@@ -68,13 +68,37 @@ class ImageService{
             'status' => 'pending',
             'created_at' => date('Y-m-d H:i:s')
         ]), ['EX' => 300]); // 5分钟过期
-        
+
         // 将任务推送到队列
         Queue::push('app\job\ImageArrJob', $params, "arrimage");
         // 返回任务ID
         return ['success' => true, 'message' => '正在生成图片中,请稍等.....', 'task_id' => $taskId];
     }
 
+    /**
+     * 图生图(Gemini:产品图+模板图)
+     * @param array $params 包含 product_img、template_img、prompt、model、id(可选)
+     * @return array
+     */
+    public function handleImgToImg($params) {
+        $id = $params['id'];
+        $time = date('YmdHis');
+        $random = mt_rand(1000, 9999);
+        $taskId = "{$id}-{$time}-{$random}";
+        $params['task_id'] = $taskId;
+
+        $redis = new \Redis();
+        $redis->connect('127.0.0.1', 6379);
+        $redis->auth('123456');
+        $redis->select(15);
+        $redis->set("img_to_img_task:{$taskId}", json_encode([
+            'status' => 'pending',
+            'created_at' => date('Y-m-d H:i:s')
+        ]), ['EX' => 300]);
+
+        Queue::push('app\job\ImageArrJob', $params, "arrimage");
+        return ['success' => true, 'message' => '正在生成图片中,请稍等.....', 'task_id' => $taskId];
+    }
 
 
     /**