import os import uuid import json import requests from typing import Optional, Dict, Any from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Path from fastapi.concurrency import run_in_threadpool from fastapi.responses import JSONResponse, FileResponse from mysql.connector.pooling import PooledMySQLConnection from mysql.connector import IntegrityError from app.core.config import settings from app.core.logger import get_logger from app.utils.scheme import CardImageResponse, ImageJsonPairResponse, ResultImagePathType from app.utils.scheme import ImageType, IMAGE_TYPE_TO_SCORE_TYPE from app.core.database_loader import get_db_connection from app.crud import crud_card logger = get_logger(__name__) router = APIRouter() db_dependency = Depends(get_db_connection) @router.post("/insert/{card_id}", response_model=CardImageResponse, status_code=201, summary="为卡牌上传并关联一张主要图片") async def upload_image_for_card( card_id: int = Path(..., description="要关联的卡牌ID"), image_type: ImageType = Form(..., description="图片类型"), image: UploadFile = File(..., description="图片文件"), image_name: Optional[str] = Form(None, description="图片的可选名称"), json_data_str: str = Form(..., description="与图片关联的JSON字符串"), db_conn: PooledMySQLConnection = db_dependency ): """ 上传一张主要图片 (coaxial/ring)。 """ # 校验:此接口只能传主图类型 if image_type in [ImageType.front_gray, ImageType.back_gray]: raise HTTPException(status_code=400, detail="此接口不支持灰度图上传,请使用 /insert/gray/{card_id}") try: detection_json = json.loads(json_data_str) except json.JSONDecodeError: raise HTTPException(status_code=400, detail="JSON格式无效。") file_extension = os.path.splitext(image.filename)[1] unique_filename = f"{uuid.uuid4()}{file_extension}" image_path = settings.DATA_DIR / unique_filename relative_path = f"/{image_path.parent.name}/{image_path.name}" try: with open(image_path, "wb") as buffer: buffer.write(await image.read()) except Exception as e: logger.error(f"保存图片失败: {e}") raise HTTPException(status_code=500, detail="无法保存图片文件。") cursor = None try: cursor = db_conn.cursor() cursor.execute(f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (card_id,)) if not cursor.fetchone(): raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌不存在。") query_insert_image = ( f"INSERT INTO {settings.DB_IMAGE_TABLE_NAME} " "(card_id, image_type, image_name, image_path, detection_json) " "VALUES (%s, %s, %s, %s, %s)" ) params = ( card_id, image_type.value, image_name, relative_path, json.dumps(detection_json, ensure_ascii=False)) cursor.execute(query_insert_image, params) new_id = cursor.lastrowid db_conn.commit() logger.info(f"图片 {new_id} 已成功关联到卡牌 {card_id},类型为 {image_type.value}。") try: crud_card.update_card_scores_and_status(db_conn, card_id) except Exception as score_update_e: logger.error(f"更新卡牌 {card_id} 分数失败: {score_update_e}") cursor.execute(f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,)) new_image_data = cursor.fetchone() columns = [desc[0] for desc in cursor.description] return CardImageResponse.model_validate(dict(zip(columns, new_image_data))) except IntegrityError as e: db_conn.rollback() if os.path.exists(image_path): os.remove(image_path) if e.errno == 1062: raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在类型为 '{image_type.value}' 的图片。") raise HTTPException(status_code=500, detail="数据库操作失败。") except Exception as e: db_conn.rollback() if os.path.exists(image_path): os.remove(image_path) logger.error(f"关联图片到卡牌失败: {e}") if isinstance(e, HTTPException): raise e raise HTTPException(status_code=500, detail="数据库操作失败。") finally: if cursor: cursor.close() @router.post("/insert/gray/{card_id}", response_model=CardImageResponse, status_code=201, summary="[NEW] 为卡牌上传辅助灰度图") async def upload_gray_image_for_card( card_id: int = Path(..., description="要关联的卡牌ID"), image_type: ImageType = Form(..., description="图片类型 (必须是 front_gray 或 back_gray)"), image: UploadFile = File(..., description="灰度图片文件"), db_conn: PooledMySQLConnection = db_dependency ): """ 上传辅助灰度图 (front_gray / back_gray)。 不需要 JSON 数据,不参与直接计算。 """ if image_type not in [ImageType.front_gray, ImageType.back_gray]: raise HTTPException(status_code=400, detail="此接口仅支持灰度图 (front_gray, back_gray)") # 1. 保存文件 file_extension = os.path.splitext(image.filename)[1] unique_filename = f"gray_{uuid.uuid4()}{file_extension}" # 加个前缀区分 image_path = settings.DATA_DIR / unique_filename relative_path = f"/{image_path.parent.name}/{image_path.name}" try: with open(image_path, "wb") as buffer: buffer.write(await image.read()) except Exception as e: logger.error(f"保存灰度图片失败: {e}") raise HTTPException(status_code=500, detail="无法保存图片文件。") cursor = None try: cursor = db_conn.cursor() # 2. 检查卡牌存在 cursor.execute(f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (card_id,)) if not cursor.fetchone(): raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌不存在。") # 3. 插入数据 (card_gray_images 表) query_insert = ( f"INSERT INTO {settings.DB_GRAY_IMAGE_TABLE_NAME} " "(card_id, image_type, image_path) VALUES (%s, %s, %s)" ) cursor.execute(query_insert, (card_id, image_type.value, relative_path)) new_id = cursor.lastrowid db_conn.commit() logger.info(f"灰度图 {new_id} 已关联到卡牌 {card_id}, 类型 {image_type.value}") # 4. 构造返回 (模拟 CardImageResponse 结构) # 因为数据库里只有基础字段,这里要补全 Pydantic 模型需要的字段 cursor.execute(f"SELECT * FROM {settings.DB_GRAY_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,)) new_data = cursor.fetchone() # tuple # 获取列名 columns = [desc[0] for desc in cursor.description] row_dict = dict(zip(columns, new_data)) # 补全空字段以符合 CardImageResponse from app.crud.crud_card import EMPTY_DETECTION_JSON response_dict = { **row_dict, "detection_json": EMPTY_DETECTION_JSON, # 默认死值 "modified_json": None, # 刚上传还没有 modified "image_name": None, "detection_image_path": None, "modified_image_path": None, "is_edited": False } return CardImageResponse.model_validate(response_dict) except IntegrityError as e: db_conn.rollback() if os.path.exists(image_path): os.remove(image_path) if e.errno == 1062: raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在类型为 '{image_type.value}' 的图片。") raise HTTPException(status_code=500, detail="数据库操作失败。") except Exception as e: db_conn.rollback() if os.path.exists(image_path): os.remove(image_path) logger.error(f"灰度图上传失败: {e}") if isinstance(e, HTTPException): raise e raise HTTPException(status_code=500, detail="数据库操作失败。") finally: if cursor: cursor.close() @router.get("/jsons/{id}", response_model=ImageJsonPairResponse, summary="获取图片的原始和修改后JSON") def get_image_jsons(id: int, db_conn: PooledMySQLConnection = db_dependency): # 此接口主要用于主图,灰度图没有实体的 JSON 存储,暂不支持直接通过此 ID 查询 JSON # 如果前端通过 format_xy 里的 query 接口获取,已经能在 list 里拿到了。 cursor = None try: cursor = db_conn.cursor(dictionary=True) query = f"SELECT id, detection_json, modified_json FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s" cursor.execute(query, (id,)) result = cursor.fetchone() if not result: raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到 (仅支持查询主图)。") return ImageJsonPairResponse.model_validate(result) except Exception as e: logger.error(f"获取JSON对失败 ({id}): {e}") if isinstance(e, HTTPException): raise e raise HTTPException(status_code=500, detail="数据库查询失败。") finally: if cursor: cursor.close()