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

340
Server/app/api/v1/logs.py Normal file
View File

@@ -0,0 +1,340 @@
# -*- coding: utf-8 -*-
"""
日志和历史记录API
"""
from typing import List, Optional
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import desc
from app.db import get_db
from app.core.security import get_current_user
from app.models.logs import PltProcessRecord, UserActionLog, PltPiece
from app.models.user import User
router = APIRouter()
# ==================== 数据模型 ====================
class ActionLogCreate(BaseModel):
"""创建操作日志"""
session_id: Optional[str] = None
action_type: str
action_params: Optional[dict] = None
page: Optional[str] = None
result: str = "success"
error_message: Optional[str] = None
duration_ms: Optional[int] = None
class ActionLogResponse(BaseModel):
"""操作日志响应"""
id: int
action_type: str
action_params: Optional[dict]
page: Optional[str]
result: str
error_message: Optional[str]
duration_ms: Optional[int]
created_at: datetime
class Config:
from_attributes = True
class PltPieceCreate(BaseModel):
"""裁片信息"""
group_id: int
size: str
image_url: Optional[str] = None
width_px: int = 0
height_px: int = 0
width_cm: float = 0
height_cm: float = 0
left_cm: float = 0
top_cm: float = 0
center_x_cm: float = 0
center_y_cm: float = 0
class PltPieceResponse(BaseModel):
"""裁片响应"""
id: int
group_id: int
size: str
image_url: Optional[str]
width_px: int
height_px: int
width_cm: float
height_cm: float
left_cm: float
top_cm: float
center_x_cm: float
center_y_cm: float
class Config:
from_attributes = True
class PltRecordCreate(BaseModel):
"""创建PLT处理记录"""
filename: Optional[str] = None
file_size: Optional[int] = None
rotated_filename: Optional[str] = None
size_labels: Optional[List[str]] = None
dpi: int = 150
rotation: int = 0
total_groups: int = 0
total_pieces: int = 0
process_time_ms: Optional[int] = None
status: str = "success"
error_message: Optional[str] = None
pieces: Optional[List[PltPieceCreate]] = None # 裁片详情
class PltRecordResponse(BaseModel):
"""PLT处理记录响应"""
id: int
filename: Optional[str]
file_size: Optional[int]
rotated_filename: Optional[str]
size_labels: Optional[List[str]]
dpi: int
rotation: int
total_groups: int
total_pieces: int
process_time_ms: Optional[int]
status: str
created_at: datetime
class Config:
from_attributes = True
class PltRecordDetailResponse(PltRecordResponse):
"""PLT处理记录详情响应包含裁片"""
pieces: List[PltPieceResponse] = []
class SessionLogsResponse(BaseModel):
"""会话日志响应给AI用"""
user_id: int
username: str
current_page: Optional[str]
recent_actions: List[ActionLogResponse]
recent_plt_records: List[PltRecordResponse]
error_count: int
# ==================== API端点 ====================
@router.post("/logs/action", response_model=ActionLogResponse)
async def create_action_log(
log_data: ActionLogCreate,
current_username: str = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""记录用户操作日志"""
# 获取用户ID
user = db.query(User).filter(User.username == current_username).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
# 创建日志
log = UserActionLog(
user_id=user.id,
session_id=log_data.session_id,
action_type=log_data.action_type,
action_params=log_data.action_params,
page=log_data.page,
result=log_data.result,
error_message=log_data.error_message,
duration_ms=log_data.duration_ms
)
db.add(log)
db.commit()
db.refresh(log)
return log
@router.post("/plt/record", response_model=PltRecordResponse)
async def create_plt_record(
record_data: PltRecordCreate,
current_username: str = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""记录PLT处理记录包含裁片详情"""
user = db.query(User).filter(User.username == current_username).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
record = PltProcessRecord(
user_id=user.id,
filename=record_data.filename,
file_size=record_data.file_size,
rotated_filename=record_data.rotated_filename,
size_labels=record_data.size_labels,
dpi=record_data.dpi,
rotation=record_data.rotation,
total_groups=record_data.total_groups,
total_pieces=record_data.total_pieces,
process_time_ms=record_data.process_time_ms,
status=record_data.status,
error_message=record_data.error_message
)
db.add(record)
db.flush() # 获取 record.id
# 保存裁片详情
if record_data.pieces:
for piece_data in record_data.pieces:
piece = PltPiece(
record_id=record.id,
group_id=piece_data.group_id,
size=piece_data.size,
image_url=piece_data.image_url,
width_px=piece_data.width_px,
height_px=piece_data.height_px,
width_cm=piece_data.width_cm,
height_cm=piece_data.height_cm,
left_cm=piece_data.left_cm,
top_cm=piece_data.top_cm,
center_x_cm=piece_data.center_x_cm,
center_y_cm=piece_data.center_y_cm
)
db.add(piece)
db.commit()
db.refresh(record)
return record
@router.get("/plt/history/{record_id}", response_model=PltRecordDetailResponse)
async def get_plt_record_detail(
record_id: int,
current_username: str = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取PLT处理记录详情包含裁片"""
user = db.query(User).filter(User.username == current_username).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
record = db.query(PltProcessRecord)\
.options(joinedload(PltProcessRecord.pieces))\
.filter(PltProcessRecord.id == record_id)\
.filter(PltProcessRecord.user_id == user.id)\
.first()
if not record:
raise HTTPException(status_code=404, detail="记录不存在")
return record
@router.get("/plt/history", response_model=List[PltRecordResponse])
async def get_plt_history(
limit: int = Query(20, ge=1, le=100),
days: int = Query(7, ge=1, le=30), # 保留天数默认7天
current_username: str = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取用户的PLT处理历史默认保留7天"""
user = db.query(User).filter(User.username == current_username).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
# 只返回指定天数内的记录
since = datetime.now() - timedelta(days=days)
records = db.query(PltProcessRecord)\
.filter(PltProcessRecord.user_id == user.id)\
.filter(PltProcessRecord.created_at >= since)\
.order_by(desc(PltProcessRecord.created_at))\
.limit(limit)\
.all()
return records
@router.get("/logs/recent", response_model=List[ActionLogResponse])
async def get_recent_logs(
limit: int = Query(50, ge=1, le=200),
action_type: Optional[str] = None,
current_username: str = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取用户最近的操作日志"""
user = db.query(User).filter(User.username == current_username).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
query = db.query(UserActionLog).filter(UserActionLog.user_id == user.id)
if action_type:
query = query.filter(UserActionLog.action_type.like(f"{action_type}%"))
logs = query.order_by(desc(UserActionLog.created_at)).limit(limit).all()
return logs
@router.get("/logs/session", response_model=SessionLogsResponse)
async def get_session_context(
hours: int = Query(24, ge=1, le=168),
current_username: str = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取会话上下文给AI助手用"""
user = db.query(User).filter(User.username == current_username).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
since = datetime.now() - timedelta(hours=hours)
# 最近的操作日志
recent_actions = db.query(UserActionLog)\
.filter(UserActionLog.user_id == user.id)\
.filter(UserActionLog.created_at >= since)\
.order_by(desc(UserActionLog.created_at))\
.limit(50)\
.all()
# 最近的PLT处理记录
recent_plt = db.query(PltProcessRecord)\
.filter(PltProcessRecord.user_id == user.id)\
.filter(PltProcessRecord.created_at >= since)\
.order_by(desc(PltProcessRecord.created_at))\
.limit(10)\
.all()
# 错误数量
error_count = db.query(UserActionLog)\
.filter(UserActionLog.user_id == user.id)\
.filter(UserActionLog.created_at >= since)\
.filter(UserActionLog.result == "error")\
.count()
# 当前页面(最近一次进入页面的记录)
last_page_action = db.query(UserActionLog)\
.filter(UserActionLog.user_id == user.id)\
.filter(UserActionLog.action_type == "page.enter")\
.order_by(desc(UserActionLog.created_at))\
.first()
current_page = last_page_action.action_params.get("page") if last_page_action and last_page_action.action_params else None
return SessionLogsResponse(
user_id=user.id,
username=current_username,
current_page=current_page,
recent_actions=recent_actions,
recent_plt_records=recent_plt,
error_count=error_count
)