# -*- 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 )