images.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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, ResultImagePathType
  14. from app.utils.scheme import ImageType, IMAGE_TYPE_TO_SCORE_TYPE
  15. from app.core.database_loader import get_db_connection
  16. from app.crud import crud_card
  17. logger = get_logger(__name__)
  18. router = APIRouter()
  19. db_dependency = Depends(get_db_connection)
  20. @router.post("/insert/{card_id}", response_model=CardImageResponse, status_code=201,
  21. summary="为卡牌上传并关联一张主要图片")
  22. async def upload_image_for_card(
  23. card_id: int = Path(..., description="要关联的卡牌ID"),
  24. image_type: ImageType = Form(..., description="图片类型"),
  25. image: UploadFile = File(..., description="图片文件"),
  26. image_name: Optional[str] = Form(None, description="图片的可选名称"),
  27. json_data_str: str = Form(..., description="与图片关联的JSON字符串"),
  28. db_conn: PooledMySQLConnection = db_dependency
  29. ):
  30. """
  31. 上传一张主要图片 (coaxial/ring)。
  32. """
  33. # 校验:此接口只能传主图类型
  34. if image_type in [ImageType.front_gray, ImageType.back_gray]:
  35. raise HTTPException(status_code=400, detail="此接口不支持灰度图上传,请使用 /insert/gray/{card_id}")
  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. cursor.execute(f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (card_id,))
  54. if not cursor.fetchone():
  55. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌不存在。")
  56. query_insert_image = (
  57. f"INSERT INTO {settings.DB_IMAGE_TABLE_NAME} "
  58. "(card_id, image_type, image_name, image_path, detection_json) "
  59. "VALUES (%s, %s, %s, %s, %s)"
  60. )
  61. params = (
  62. card_id, image_type.value, image_name, relative_path, json.dumps(detection_json, ensure_ascii=False))
  63. cursor.execute(query_insert_image, params)
  64. new_id = cursor.lastrowid
  65. db_conn.commit()
  66. logger.info(f"图片 {new_id} 已成功关联到卡牌 {card_id},类型为 {image_type.value}。")
  67. try:
  68. crud_card.update_card_scores_and_status(db_conn, card_id)
  69. except Exception as score_update_e:
  70. logger.error(f"更新卡牌 {card_id} 分数失败: {score_update_e}")
  71. cursor.execute(f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,))
  72. new_image_data = cursor.fetchone()
  73. columns = [desc[0] for desc in cursor.description]
  74. return CardImageResponse.model_validate(dict(zip(columns, new_image_data)))
  75. except IntegrityError as e:
  76. db_conn.rollback()
  77. if os.path.exists(image_path): os.remove(image_path)
  78. if e.errno == 1062:
  79. raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在类型为 '{image_type.value}' 的图片。")
  80. raise HTTPException(status_code=500, detail="数据库操作失败。")
  81. except Exception as e:
  82. db_conn.rollback()
  83. if os.path.exists(image_path): os.remove(image_path)
  84. logger.error(f"关联图片到卡牌失败: {e}")
  85. if isinstance(e, HTTPException): raise e
  86. raise HTTPException(status_code=500, detail="数据库操作失败。")
  87. finally:
  88. if cursor: cursor.close()
  89. @router.post("/insert/gray/{card_id}", response_model=CardImageResponse, status_code=201,
  90. summary="[NEW] 为卡牌上传辅助灰度图")
  91. async def upload_gray_image_for_card(
  92. card_id: int = Path(..., description="要关联的卡牌ID"),
  93. image_type: ImageType = Form(..., description="图片类型 (必须是 front_gray 或 back_gray)"),
  94. image: UploadFile = File(..., description="灰度图片文件"),
  95. db_conn: PooledMySQLConnection = db_dependency
  96. ):
  97. """
  98. 上传辅助灰度图 (front_gray / back_gray)。
  99. 不需要 JSON 数据,不参与直接计算。
  100. """
  101. if image_type not in [ImageType.front_gray, ImageType.back_gray]:
  102. raise HTTPException(status_code=400, detail="此接口仅支持灰度图 (front_gray, back_gray)")
  103. # 1. 保存文件
  104. file_extension = os.path.splitext(image.filename)[1]
  105. unique_filename = f"gray_{uuid.uuid4()}{file_extension}" # 加个前缀区分
  106. image_path = settings.DATA_DIR / unique_filename
  107. relative_path = f"/{image_path.parent.name}/{image_path.name}"
  108. try:
  109. with open(image_path, "wb") as buffer:
  110. buffer.write(await image.read())
  111. except Exception as e:
  112. logger.error(f"保存灰度图片失败: {e}")
  113. raise HTTPException(status_code=500, detail="无法保存图片文件。")
  114. cursor = None
  115. try:
  116. cursor = db_conn.cursor()
  117. # 2. 检查卡牌存在
  118. cursor.execute(f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (card_id,))
  119. if not cursor.fetchone():
  120. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌不存在。")
  121. # 3. 插入数据 (card_gray_images 表)
  122. query_insert = (
  123. f"INSERT INTO {settings.DB_GRAY_IMAGE_TABLE_NAME} "
  124. "(card_id, image_type, image_path) VALUES (%s, %s, %s)"
  125. )
  126. cursor.execute(query_insert, (card_id, image_type.value, relative_path))
  127. new_id = cursor.lastrowid
  128. db_conn.commit()
  129. logger.info(f"灰度图 {new_id} 已关联到卡牌 {card_id}, 类型 {image_type.value}")
  130. # 4. 构造返回 (模拟 CardImageResponse 结构)
  131. # 因为数据库里只有基础字段,这里要补全 Pydantic 模型需要的字段
  132. cursor.execute(f"SELECT * FROM {settings.DB_GRAY_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,))
  133. new_data = cursor.fetchone() # tuple
  134. # 获取列名
  135. columns = [desc[0] for desc in cursor.description]
  136. row_dict = dict(zip(columns, new_data))
  137. # 补全空字段以符合 CardImageResponse
  138. from app.crud.crud_card import EMPTY_DETECTION_JSON
  139. response_dict = {
  140. **row_dict,
  141. "detection_json": EMPTY_DETECTION_JSON, # 默认死值
  142. "modified_json": None, # 刚上传还没有 modified
  143. "image_name": None,
  144. "detection_image_path": None,
  145. "modified_image_path": None,
  146. "is_edited": False
  147. }
  148. return CardImageResponse.model_validate(response_dict)
  149. except IntegrityError as e:
  150. db_conn.rollback()
  151. if os.path.exists(image_path): os.remove(image_path)
  152. if e.errno == 1062:
  153. raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在类型为 '{image_type.value}' 的图片。")
  154. raise HTTPException(status_code=500, detail="数据库操作失败。")
  155. except Exception as e:
  156. db_conn.rollback()
  157. if os.path.exists(image_path): os.remove(image_path)
  158. logger.error(f"灰度图上传失败: {e}")
  159. if isinstance(e, HTTPException): raise e
  160. raise HTTPException(status_code=500, detail="数据库操作失败。")
  161. finally:
  162. if cursor: cursor.close()
  163. @router.get("/jsons/{id}", response_model=ImageJsonPairResponse, summary="获取图片的原始和修改后JSON")
  164. def get_image_jsons(id: int, db_conn: PooledMySQLConnection = db_dependency):
  165. # 此接口主要用于主图,灰度图没有实体的 JSON 存储,暂不支持直接通过此 ID 查询 JSON
  166. # 如果前端通过 format_xy 里的 query 接口获取,已经能在 list 里拿到了。
  167. cursor = None
  168. try:
  169. cursor = db_conn.cursor(dictionary=True)
  170. query = f"SELECT id, detection_json, modified_json FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s"
  171. cursor.execute(query, (id,))
  172. result = cursor.fetchone()
  173. if not result:
  174. raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到 (仅支持查询主图)。")
  175. return ImageJsonPairResponse.model_validate(result)
  176. except Exception as e:
  177. logger.error(f"获取JSON对失败 ({id}): {e}")
  178. if isinstance(e, HTTPException): raise e
  179. raise HTTPException(status_code=500, detail="数据库查询失败。")
  180. finally:
  181. if cursor: cursor.close()