images.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import os
  2. import uuid
  3. import json
  4. import requests
  5. from typing import Optional, Dict, Any
  6. from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Path
  7. from fastapi.concurrency import run_in_threadpool
  8. from fastapi.responses import JSONResponse, FileResponse
  9. from mysql.connector.pooling import PooledMySQLConnection
  10. from mysql.connector import IntegrityError
  11. from app.core.config import settings
  12. from app.core.logger import get_logger
  13. from app.utils.scheme import CardImageResponse, ImageJsonPairResponse
  14. from app.utils.scheme import ImageType, IMAGE_TYPE_TO_SCORE_TYPE
  15. from app.core.database_loader import get_db_connection
  16. logger = get_logger(__name__)
  17. router = APIRouter()
  18. db_dependency = Depends(get_db_connection)
  19. @router.post("/insert/{card_id}", response_model=CardImageResponse, status_code=201,
  20. summary="为卡牌上传并关联一张图片")
  21. async def upload_image_for_card(
  22. card_id: int = Path(..., description="要关联的卡牌ID"),
  23. image_type: ImageType = Form(..., description="图片类型 (front_face, back_face, etc.)"),
  24. image: UploadFile = File(..., description="图片文件"),
  25. image_name: Optional[str] = Form(None, description="图片的可选名称"),
  26. json_data_str: str = Form(..., description="与图片关联的JSON字符串"),
  27. db_conn: PooledMySQLConnection = db_dependency
  28. ):
  29. """
  30. 上传一张图片,并将其作为一条新记录存入 card_images 表。
  31. 这是一个事务性操作,并会检查是否存在重复的 (card_id, image_type) 组合。
  32. """
  33. try:
  34. detection_json = json.loads(json_data_str)
  35. except json.JSONDecodeError:
  36. raise HTTPException(status_code=400, detail="JSON格式无效。")
  37. file_extension = os.path.splitext(image.filename)[1]
  38. unique_filename = f"{uuid.uuid4()}{file_extension}"
  39. image_path = settings.DATA_DIR / unique_filename
  40. relative_path = f"/{image_path.parent.name}/{image_path.name}"
  41. try:
  42. with open(image_path, "wb") as buffer:
  43. buffer.write(await image.read())
  44. except Exception as e:
  45. logger.error(f"保存图片失败: {e}")
  46. raise HTTPException(status_code=500, detail="无法保存图片文件。")
  47. cursor = None
  48. try:
  49. cursor = db_conn.cursor()
  50. # 检查 card_id 是否存在
  51. cursor.execute(f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (card_id,))
  52. if not cursor.fetchone():
  53. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌不存在。")
  54. query_insert_image = (
  55. f"INSERT INTO {settings.DB_IMAGE_TABLE_NAME} "
  56. "(card_id, image_type, image_name, image_path, detection_json) "
  57. "VALUES (%s, %s, %s, %s, %s)"
  58. )
  59. params = (
  60. card_id, image_type.value, image_name, relative_path, json.dumps(detection_json, ensure_ascii=False))
  61. cursor.execute(query_insert_image, params)
  62. new_id = cursor.lastrowid
  63. db_conn.commit()
  64. logger.info(f"图片 {new_id} 已成功关联到卡牌 {card_id},类型为 {image_type.value}。")
  65. cursor.execute(f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,))
  66. new_image_data = cursor.fetchone()
  67. columns = [desc[0] for desc in cursor.description]
  68. return CardImageResponse.model_validate(dict(zip(columns, new_image_data)))
  69. except IntegrityError as e:
  70. db_conn.rollback()
  71. if os.path.exists(image_path): os.remove(image_path)
  72. if e.errno == 1062:
  73. raise HTTPException(
  74. status_code=409,
  75. detail=f"卡牌ID {card_id} 已存在类型为 '{image_type.value}' 的图片,请勿重复添加。"
  76. )
  77. logger.error(f"数据库完整性错误: {e}")
  78. raise HTTPException(status_code=500, detail="数据库操作失败。")
  79. except Exception as e:
  80. db_conn.rollback()
  81. if os.path.exists(image_path):
  82. os.remove(image_path)
  83. logger.error(f"关联图片到卡牌失败: {e}")
  84. if isinstance(e, HTTPException): raise e
  85. raise HTTPException(status_code=500, detail="数据库操作失败,所有更改已回滚。")
  86. finally:
  87. if cursor:
  88. cursor.close()
  89. @router.get("/jsons/{id}", response_model=ImageJsonPairResponse, summary="获取图片的原始和修改后JSON")
  90. def get_image_jsons(id: int, db_conn: PooledMySQLConnection = db_dependency):
  91. """获取指定图片ID的 detection_json 和 modified_json。"""
  92. cursor = None
  93. try:
  94. cursor = db_conn.cursor(dictionary=True)
  95. query = f"SELECT id, detection_json, modified_json FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s"
  96. cursor.execute(query, (id,))
  97. result = cursor.fetchone()
  98. if not result:
  99. raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
  100. return ImageJsonPairResponse.model_validate(result)
  101. except Exception as e:
  102. logger.error(f"获取JSON对失败 ({id}): {e}")
  103. if isinstance(e, HTTPException): raise e
  104. raise HTTPException(status_code=500, detail="数据库查询失败。")
  105. finally:
  106. if cursor:
  107. cursor.close()
  108. @router.put("/update/json/{id}", status_code=200, summary="重新计算分数, 并修改图片的 modified_json")
  109. async def update_image_modified_json(
  110. id: int,
  111. new_json_data: dict,
  112. db_conn: PooledMySQLConnection = db_dependency
  113. ):
  114. """
  115. 根据 id 获取 image_type, 调用外部接口重新计算分数, 并更新 modified_json。
  116. updated_at 会自动更新
  117. """
  118. cursor = None
  119. try:
  120. cursor = db_conn.cursor(dictionary=True)
  121. # 1️ 获取图片信息
  122. cursor.execute(f"SELECT image_type FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (id,))
  123. row = cursor.fetchone()
  124. if not row:
  125. raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
  126. image_type = row["image_type"]
  127. score_type = IMAGE_TYPE_TO_SCORE_TYPE.get(image_type)
  128. if not score_type:
  129. raise HTTPException(status_code=400, detail=f"未知的 image_type: {image_type}")
  130. # 2️ 调用远程计算接口
  131. try:
  132. params = {"score_type": score_type}
  133. payload = new_json_data
  134. response = await run_in_threadpool(
  135. lambda: requests.post(
  136. settings.SCORE_RECALCULATE_ENDPOINT,
  137. params=params,
  138. json=payload,
  139. timeout=30
  140. )
  141. )
  142. except Exception as e:
  143. raise HTTPException(status_code=500, detail=f"调用分数计算服务失败: {e}")
  144. if response.status_code != 200:
  145. logger.error(f"分数计算接口返回错误: {response.status_code}, {response.text}")
  146. raise HTTPException(status_code=response.status_code,
  147. detail=f"分数计算接口返回错误: {response.text}")
  148. recalculated_json = response.json()
  149. # 3️ 保存结果到数据库
  150. recalculated_json_str = json.dumps(recalculated_json, ensure_ascii=False)
  151. update_query = f"UPDATE {settings.DB_IMAGE_TABLE_NAME} SET modified_json = %s WHERE id = %s"
  152. cursor.execute(update_query, (recalculated_json_str, id))
  153. if cursor.rowcount == 0:
  154. raise HTTPException(status_code=404, detail=f"未找到ID {id} 的记录。")
  155. db_conn.commit()
  156. logger.info(f"图片ID {id} 的 modified_json 已更新并重新计算。")
  157. return {
  158. "message": f"成功更新图片ID {id} 的JSON数据",
  159. "image_type": image_type,
  160. "score_type": score_type
  161. }
  162. except HTTPException:
  163. db_conn.rollback()
  164. raise
  165. except Exception as e:
  166. db_conn.rollback()
  167. logger.error(f"更新JSON失败 ({id}): {e}")
  168. raise HTTPException(status_code=500, detail=f"更新JSON数据失败: {e}")
  169. finally:
  170. if cursor:
  171. cursor.close()
  172. @router.get("/image_file/{id}", summary="获取指定ID的图片文件")
  173. def get_image_file(id: int, db_conn: PooledMySQLConnection = db_dependency):
  174. """根据 id 查找记录,并返回对应的图片文件。"""
  175. cursor = None
  176. try:
  177. cursor = db_conn.cursor()
  178. query = f"SELECT image_path FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s"
  179. cursor.execute(query, (id,))
  180. result = cursor.fetchone()
  181. if not result:
  182. raise HTTPException(status_code=404, detail=f"ID为 {id} 的记录未找到。")
  183. image_path = result[0]
  184. absolute_path = settings.BASE_PATH / image_path.lstrip('/\\')
  185. if not os.path.exists(absolute_path):
  186. logger.error(f"文件在服务器上未找到: {absolute_path} (数据库ID: {id})")
  187. raise HTTPException(status_code=404, detail="图片文件在服务器上不存在。")
  188. return FileResponse(absolute_path)
  189. except Exception as e:
  190. logger.error(f"获取图片失败 ({id}): {e}")
  191. if isinstance(e, HTTPException): raise e
  192. raise HTTPException(status_code=500, detail="获取图片文件失败。")
  193. finally:
  194. if cursor:
  195. cursor.close()