| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- 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()
|