images.py 7.6 KB

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