feat: AI套图分层方案 + Gemini集成 - 4种图案类型处理 + 正片叠底 + 宽高比 + 模型选择
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 配置 — 通义千问 Qwen(DashScope)
|
||||
AI_API_KEY: str = "" # LLM API Key(OpenAI / 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()
|
||||
|
||||
154
Server/app/core/qiniu_storage.py
Normal file
154
Server/app/core/qiniu_storage.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user