images.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import os
  2. import uuid
  3. import json
  4. from typing import Optional, Dict, Any
  5. from enum import Enum
  6. from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Path
  7. from fastapi.responses import JSONResponse, FileResponse
  8. from mysql.connector.pooling import PooledMySQLConnection
  9. from app.core.config import settings
  10. from app.core.logger import get_logger
  11. from app.utils.scheme import CardImageResponse, ImageJsonPairResponse
  12. from app.core.database_loader import get_db_connection
  13. logger = get_logger(__name__)
  14. router = APIRouter()
  15. db_dependency = Depends(get_db_connection)
  16. class ImageType(str, Enum):
  17. front_face = "front_face"
  18. back_face = "back_face"
  19. front_edge = "front_edge"
  20. back_edge = "back_edge"
  21. @router.post("/insert/{card_id}", response_model=CardImageResponse, status_code=201,
  22. summary="为卡牌上传并关联一张图片")
  23. async def upload_image_for_card(
  24. card_id: int = Path(..., description="要关联的卡牌ID"),
  25. image_type: ImageType = Form(..., description="图片类型 (front_face, back_face, etc.)"),
  26. image: UploadFile = File(..., description="图片文件"),
  27. json_data_str: str = Form(..., description="与图片关联的JSON字符串"),
  28. image_name: Optional[str] = Form(None, description="图片的可选名称"),
  29. db_conn: PooledMySQLConnection = db_dependency
  30. ):
  31. """
  32. 上传一张图片,将其存入 card_images 表,并更新 cards 表中对应的外键字段。
  33. 这是一个事务性操作。
  34. """
  35. try:
  36. detection_json = json.loads(json_data_str)
  37. except json.JSONDecodeError:
  38. raise HTTPException(status_code=400, detail="JSON格式无效。")
  39. # 保存图片文件
  40. file_extension = os.path.splitext(image.filename)[1]
  41. unique_filename = f"{uuid.uuid4()}{file_extension}"
  42. image_path = settings.DATA_DIR / unique_filename
  43. try:
  44. with open(image_path, "wb") as buffer:
  45. buffer.write(await image.read())
  46. except Exception as e:
  47. logger.error(f"保存图片失败: {e}")
  48. raise HTTPException(status_code=500, detail="无法保存图片文件。")
  49. cursor = None
  50. try:
  51. cursor = db_conn.cursor()
  52. # 1. 插入图片记录到 card_images
  53. query_insert_image = (
  54. f"INSERT INTO {settings.DB_IMAGE_TABLE_NAME} (image_name, image_path, detection_json) "
  55. "VALUES (%s, %s, %s)"
  56. )
  57. params = (image_name, str(image_path), json.dumps(detection_json, ensure_ascii=False))
  58. cursor.execute(query_insert_image, params)
  59. new_image_id = cursor.lastrowid
  60. # 2. 更新 cards 表,将新图片ID设置到对应字段
  61. # image_type.value 的值是 "front_face", "back_face" 等
  62. column_to_update = f"{image_type.value}_id"
  63. query_update_card = (
  64. f"UPDATE {settings.DB_CARD_TABLE_NAME} SET {column_to_update} = %s "
  65. f"WHERE card_id = %s"
  66. )
  67. cursor.execute(query_update_card, (new_image_id, card_id))
  68. if cursor.rowcount == 0:
  69. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌未找到,操作已回滚。")
  70. db_conn.commit()
  71. logger.info(f"图片 {new_image_id} 已成功关联到卡牌 {card_id} 的 {image_type.value}。")
  72. # 查询并返回完整的图片记录
  73. cursor.execute(f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE image_id = %s", (new_image_id,))
  74. new_image_data = cursor.fetchone()
  75. columns = [desc[0] for desc in cursor.description]
  76. return CardImageResponse.model_validate(dict(zip(columns, new_image_data)))
  77. except Exception as e:
  78. db_conn.rollback()
  79. if os.path.exists(image_path):
  80. os.remove(image_path) # 如果数据库操作失败,删除已上传的文件
  81. logger.error(f"关联图片到卡牌失败: {e}")
  82. if isinstance(e, HTTPException): raise e
  83. raise HTTPException(status_code=500, detail="数据库操作失败,所有更改已回滚。")
  84. finally:
  85. if cursor:
  86. cursor.close()
  87. @router.get("/jsons/{image_id}", response_model=ImageJsonPairResponse, summary="获取图片的原始和修改后JSON")
  88. def get_image_jsons(image_id: int, db_conn: PooledMySQLConnection = db_dependency):
  89. """获取指定图片ID的 detection_json 和 modified_json。"""
  90. cursor = None
  91. try:
  92. cursor = db_conn.cursor(dictionary=True)
  93. query = f"SELECT image_id, detection_json, modified_json FROM {settings.DB_IMAGE_TABLE_NAME} WHERE image_id = %s"
  94. cursor.execute(query, (image_id,))
  95. result = cursor.fetchone()
  96. if not result:
  97. raise HTTPException(status_code=404, detail=f"ID为 {image_id} 的图片未找到。")
  98. return ImageJsonPairResponse.model_validate(result)
  99. except Exception as e:
  100. logger.error(f"获取JSON对失败 ({image_id}): {e}")
  101. if isinstance(e, HTTPException): raise e
  102. raise HTTPException(status_code=500, detail="数据库查询失败。")
  103. finally:
  104. if cursor:
  105. cursor.close()
  106. @router.put("/update/json/{image_id}", status_code=200, summary="7. 修改图片的 modified_json")
  107. def update_image_modified_json(
  108. image_id: int,
  109. new_json_data: Dict[str, Any],
  110. db_conn: PooledMySQLConnection = db_dependency
  111. ):
  112. """根据 image_id 更新 modified_json 字段。updated_at 会自动更新。"""
  113. cursor = None
  114. try:
  115. cursor = db_conn.cursor()
  116. new_json_str = json.dumps(new_json_data, ensure_ascii=False)
  117. # updated_at 会由数据库自动更新
  118. query = f"UPDATE {settings.DB_IMAGE_TABLE_NAME} SET modified_json = %s WHERE image_id = %s"
  119. cursor.execute(query, (new_json_str, image_id))
  120. if cursor.rowcount == 0:
  121. raise HTTPException(status_code=404, detail=f"ID为 {image_id} 的图片未找到。")
  122. db_conn.commit()
  123. logger.info(f"图片ID {image_id} 的 modified_json 已更新。")
  124. return {"message": f"成功更新图片ID {image_id} 的JSON数据"}
  125. except Exception as e:
  126. db_conn.rollback()
  127. logger.error(f"更新JSON失败 ({image_id}): {e}")
  128. if isinstance(e, HTTPException): raise e
  129. raise HTTPException(status_code=500, detail="更新JSON数据失败。")
  130. finally:
  131. if cursor:
  132. cursor.close()
  133. @router.get("/image_file/{image_id}", summary="获取指定ID的图片文件")
  134. def get_image_file(image_id: int, db_conn: PooledMySQLConnection = db_dependency):
  135. """根据 image_id 查找记录,并返回对应的图片文件。"""
  136. cursor = None
  137. try:
  138. cursor = db_conn.cursor()
  139. query = f"SELECT image_path FROM {settings.DB_IMAGE_TABLE_NAME} WHERE image_id = %s"
  140. cursor.execute(query, (image_id,))
  141. result = cursor.fetchone()
  142. if not result:
  143. raise HTTPException(status_code=404, detail=f"ID为 {image_id} 的记录未找到。")
  144. image_path = result[0]
  145. if not os.path.exists(image_path):
  146. logger.error(f"文件在服务器上未找到: {image_path} (数据库ID: {image_id})")
  147. raise HTTPException(status_code=404, detail="图片文件在服务器上不存在。")
  148. return FileResponse(image_path)
  149. except Exception as e:
  150. logger.error(f"获取图片失败 ({image_id}): {e}")
  151. if isinstance(e, HTTPException): raise e
  152. raise HTTPException(status_code=500, detail="获取图片文件失败。")
  153. finally:
  154. if cursor:
  155. cursor.close()