feat: AI套图分层方案 + Gemini集成 - 4种图案类型处理 + 正片叠底 + 宽高比 + 模型选择

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-07 16:59:56 +08:00
parent 12395d8eca
commit dae906aba7
277 changed files with 15009 additions and 19922 deletions

View File

@@ -5,20 +5,37 @@ class Settings(BaseSettings):
PROJECT_NAME: str = "DesignerCEP Backend"
API_V1_STR: str = "/api/v1"
DATABASE_URL: str = "sqlite:///./designercep.db"
SECRET_KEY: str = "change-me"
SECRET_KEY: str = "" # 必须从 .env 读取
ACCESS_TOKEN_EXPIRE_MINUTES: int = 10080
ALLOWED_ORIGINS: str = "*"
ADMIN_TOKEN: str = "admin-token"
ADMIN_TOKEN: str = "" # 必须从 .env 读取
# Email Configuration
SMTP_HOST: str = "smtp.gmail.com"
SMTP_PORT: int = 587
SMTP_USER: str = "ly1104803132@gmail.com"
SMTP_PASSWORD: str = "wsfrpnmkojpsqdkk"
EMAILS_FROM_EMAIL: str = "ly1104803132@gmail.com"
SMTP_USER: str = ""
SMTP_PASSWORD: str = "" # 必须从 .env 读取
EMAILS_FROM_EMAIL: str = ""
EMAILS_FROM_NAME: str = "Designer"
# 七牛云对象存储
QINIU_ACCESS_KEY: str = ""
QINIU_SECRET_KEY: str = ""
QINIU_BUCKET: str = ""
QINIU_DOMAIN: str = ""
# AI 配置 — 通义千问 QwenDashScope
AI_API_KEY: str = "" # LLM API KeyOpenAI / DeepSeek 等)
AI_BASE_URL: str = "" # LLM API 地址(留空则用 OpenAI 默认)
AI_MODEL: str = "qwen3-max-2026-01-23" # 默认模型通义千问3深度思考
AI_VISION_MODEL: str = "qwen-vl-max-latest" # 视觉分析模型(看图分析成衣)
AI_IMAGE_EDIT_MODEL: str = "qwen-image-edit-max-2026-01-16" # 图片编辑/生成模型
# AI 配置 — Gemini第三方代理
GEMINI_API_KEY: str = ""
GEMINI_BASE_URL: str = "https://api.apiqik.online"
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
settings = Settings()

View File

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
"""
七牛云对象存储工具
"""
import base64
import hashlib
from datetime import datetime
from typing import Optional, Tuple
from qiniu import Auth, put_data, BucketManager
from app.core.config import settings
# ==================== 配置 ====================
# 从 settings 读取(通过 pydantic-settings 加载 .env
QINIU_ACCESS_KEY = settings.QINIU_ACCESS_KEY
QINIU_SECRET_KEY = settings.QINIU_SECRET_KEY
QINIU_BUCKET = settings.QINIU_BUCKET
QINIU_DOMAIN = settings.QINIU_DOMAIN # 例如: http://cdn.example.com
# 是否启用七牛云存储
QINIU_ENABLED = bool(QINIU_ACCESS_KEY and QINIU_SECRET_KEY and QINIU_BUCKET and QINIU_DOMAIN)
class QiniuStorage:
"""七牛云存储工具类"""
def __init__(self):
self.enabled = QINIU_ENABLED
if self.enabled:
self.auth = Auth(QINIU_ACCESS_KEY, QINIU_SECRET_KEY)
self.bucket = QINIU_BUCKET
self.domain = QINIU_DOMAIN.rstrip('/')
print(f"✅ 七牛云存储已启用Bucket: {self.bucket}")
else:
self.auth = None
self.bucket = None
self.domain = None
print("⚠️ 七牛云存储未配置,图片将使用 base64 返回")
def upload_base64(
self,
base64_data: str,
key_prefix: str = "plt",
user_id: Optional[int] = None
) -> Tuple[bool, str]:
"""
上传 base64 图片到七牛云
Args:
base64_data: base64 编码的图片数据(可带或不带 data:image/png;base64, 前缀)
key_prefix: 文件名前缀
user_id: 用户ID用于分目录
Returns:
(success, url_or_error): 成功返回 URL失败返回错误信息
"""
if not self.enabled:
return False, "七牛云存储未启用"
try:
# 去掉 base64 前缀
if ',' in base64_data:
base64_data = base64_data.split(',')[1]
# 解码
image_data = base64.b64decode(base64_data)
# 生成文件名: plt/2024/02/05/u123/时间戳_哈希.png
now = datetime.now()
date_path = now.strftime("%Y/%m/%d")
timestamp = now.strftime("%H%M%S") # 时分秒
content_hash = hashlib.md5(image_data).hexdigest()[:8]
if user_id:
key = f"{key_prefix}/{date_path}/u{user_id}/{timestamp}_{content_hash}.png"
else:
key = f"{key_prefix}/{date_path}/{timestamp}_{content_hash}.png"
# 生成上传凭证
token = self.auth.upload_token(self.bucket, key, 3600)
# 上传
ret, info = put_data(token, key, image_data)
if info.status_code == 200:
url = f"{self.domain}/{key}"
return True, url
else:
print(f"[七牛云] 上传失败: status={info.status_code}, error={info.error}, text={info.text_body}")
return False, f"上传失败: {info.error}"
except Exception as e:
print(f"[七牛云] 上传异常: {str(e)}")
return False, f"上传异常: {str(e)}"
def upload_batch(
self,
images: list, # [(base64_data, name), ...]
key_prefix: str = "plt",
user_id: Optional[int] = None
) -> list:
"""
批量上传图片
Returns:
[(success, url_or_base64, name), ...]
"""
results = []
success_count = 0
fail_count = 0
for base64_data, name in images:
if self.enabled:
success, result = self.upload_base64(base64_data, key_prefix, user_id)
if success:
results.append((True, result, name))
success_count += 1
else:
# 上传失败,回退到 base64
print(f"[七牛云] 上传失败 {name}: {result}")
results.append((False, base64_data, name))
fail_count += 1
else:
# 未启用,返回原始 base64
results.append((False, base64_data, name))
if fail_count > 0:
print(f"[七牛云] 批量上传结果: 成功 {success_count}, 失败 {fail_count}")
return results
def delete(self, key: str) -> bool:
"""删除文件"""
if not self.enabled:
return False
try:
bucket_manager = BucketManager(self.auth)
ret, info = bucket_manager.delete(self.bucket, key)
return info.status_code == 200
except Exception:
return False
def get_url(self, key: str) -> str:
"""获取文件 URL"""
if not self.enabled or not key:
return ""
return f"{self.domain}/{key}"
# 全局实例
qiniu_storage = QiniuStorage()

View File

@@ -34,10 +34,6 @@ def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(
验证 JWT Token 并返回当前用户名 (sub)
增加 Session 强校验:检查数据库中 Session 是否活跃
"""
print("="*60)
print("[get_current_user] 开始验证 Token")
print(f" - Token (前30字符): {token[:30]}...")
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭据",
@@ -50,41 +46,24 @@ def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(
username: str = payload.get("sub")
device_id: str = payload.get("device_id")
print(f"[get_current_user] ✓ Token 解析成功")
print(f" - username: {username}")
print(f" - device_id: {device_id}")
print(f" - exp: {payload.get('exp')}")
if username is None:
print("[get_current_user] ✗ username 为空")
raise credentials_exception
except jwt.ExpiredSignatureError:
print("[get_current_user] ✗ Token 已过期")
raise credentials_exception
except jwt.InvalidTokenError as e:
print(f"[get_current_user] ✗ Token 无效: {e}")
except jwt.InvalidTokenError:
raise credentials_exception
except jwt.PyJWTError as e:
print(f"[get_current_user] ✗ Token 解析失败: {e}")
except jwt.PyJWTError:
raise credentials_exception
# 2. Session 强校验 (如果有 device_id)
# 如果 Token 是旧版本没有 device_id可以选择放行或拒绝。为了安全建议逐步拒绝。
# 这里我们假设所有新 Token 都有 device_id
if device_id:
print(f"[get_current_user] 开始 Session 强校验")
# 查询 User ID
from app.models.user import User
user = db.query(User).filter(User.username == username).first()
if not user:
print(f"[get_current_user] ✗ 用户不存在: {username}")
raise credentials_exception
print(f"[get_current_user] ✓ 用户存在: user_id={user.id}")
# 查询活跃 Session
print(f"[get_current_user] 查询活跃 Session: user_id={user.id}, device_id={device_id}")
session = db.query(UserSession).filter(
UserSession.user_id == user.id,
UserSession.device_id == device_id,
@@ -92,24 +71,10 @@ def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(
).first()
if not session:
# Session 不存在或已失效(被踢下线/登出)
print(f"[get_current_user] ✗ Session 不存在或已失效")
# 调试:列出该用户的所有 Session
all_sessions = db.query(UserSession).filter(UserSession.user_id == user.id).all()
print(f"[get_current_user] 该用户共有 {len(all_sessions)} 个 Session:")
for s in all_sessions:
print(f" - session_id={s.id}, device_id={s.device_id}, active={s.active}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="会话已失效或在其他设备登录",
headers={"WWW-Authenticate": "Bearer"},
)
print(f"[get_current_user] ✓ Session 验证通过: session_id={session.id}")
else:
print(f"[get_current_user] ⚠️ Token 中没有 device_id跳过 Session 校验")
print("="*60)
return username