images.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import os
  2. import uuid
  3. import json
  4. import requests
  5. import io
  6. from app.core.minio_client import minio_client
  7. from typing import Optional, Dict, Any
  8. from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Path
  9. from fastapi.concurrency import run_in_threadpool
  10. from fastapi.responses import JSONResponse, FileResponse
  11. from mysql.connector.pooling import PooledMySQLConnection
  12. from mysql.connector import IntegrityError
  13. from app.core.config import settings
  14. from app.core.logger import get_logger
  15. from app.utils.scheme import CardImageResponse, ImageJsonPairResponse, ResultImagePathType
  16. from app.utils.scheme import ImageType, IMAGE_TYPE_TO_SCORE_TYPE
  17. from app.core.database_loader import get_db_connection
  18. from app.crud import crud_card
  19. logger = get_logger(__name__)
  20. router = APIRouter()
  21. db_dependency = Depends(get_db_connection)
  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="图片类型"),
  27. image: UploadFile = File(..., description="图片文件"),
  28. image_name: Optional[str] = Form(None, description="图片的可选名称"),
  29. json_data_str: Optional[str] = Form(None, description="与图片关联的JSON字符串"),
  30. json_data_file: Optional[UploadFile] = File(None, description="与图片关联的JSON文件"),
  31. db_conn: PooledMySQLConnection = db_dependency
  32. ):
  33. """
  34. 上传一张主要图片 (coaxial/ring)。
  35. """
  36. # 校验:此接口只能传主图类型
  37. if image_type in [ImageType.front_gray, ImageType.back_gray]:
  38. raise HTTPException(status_code=400, detail="此接口不支持灰度图上传,请使用 /insert/gray/{card_id}")
  39. if json_data_file is not None:
  40. json_data_bytes = await json_data_file.read()
  41. json_payload = json_data_bytes.decode("utf-8")
  42. else:
  43. json_payload = json_data_str
  44. if not json_payload:
  45. raise HTTPException(status_code=400, detail="缺少JSON数据。")
  46. try:
  47. detection_json = json.loads(json_payload)
  48. except json.JSONDecodeError:
  49. raise HTTPException(status_code=400, detail="JSON格式无效。")
  50. file_extension = os.path.splitext(image.filename)[1]
  51. unique_filename = f"{uuid.uuid4()}{file_extension}"
  52. relative_path = f"/Data/{unique_filename}"
  53. object_name = f"{settings.MINIO_BASE_PREFIX}{relative_path}"
  54. try:
  55. # 写入 MinIO 存储
  56. file_bytes = await image.read()
  57. minio_client.put_object(
  58. settings.MINIO_BUCKET,
  59. object_name,
  60. io.BytesIO(file_bytes),
  61. len(file_bytes),
  62. content_type=image.content_type or "image/jpeg"
  63. )
  64. except Exception as e:
  65. logger.error(f"保存图片到MinIO失败: {e}")
  66. raise HTTPException(status_code=500, detail="无法保存图片文件。")
  67. cursor = None
  68. try:
  69. cursor = db_conn.cursor()
  70. cursor.execute(f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (card_id,))
  71. if not cursor.fetchone():
  72. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌不存在。")
  73. query_insert_image = (
  74. f"INSERT INTO {settings.DB_IMAGE_TABLE_NAME} "
  75. "(card_id, image_type, image_name, image_path, detection_json) "
  76. "VALUES (%s, %s, %s, %s, %s)"
  77. )
  78. params = (
  79. card_id, image_type.value, image_name, relative_path, json.dumps(detection_json, ensure_ascii=False))
  80. cursor.execute(query_insert_image, params)
  81. new_id = cursor.lastrowid
  82. db_conn.commit()
  83. logger.info(f"图片 {new_id} 已成功关联到卡牌 {card_id},类型为 {image_type.value}。")
  84. try:
  85. crud_card.update_card_scores_and_status(db_conn, card_id)
  86. except Exception as score_update_e:
  87. logger.error(f"更新卡牌 {card_id} 分数失败: {score_update_e}")
  88. cursor.execute(f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,))
  89. new_image_data = cursor.fetchone()
  90. columns = [desc[0] for desc in cursor.description]
  91. row_dict = dict(zip(columns, new_image_data))
  92. # 返回完整url
  93. row_dict['image_path'] = settings.get_full_url(row_dict.get('image_path'))
  94. row_dict['detection_image_path'] = settings.get_full_url(row_dict.get('detection_image_path'))
  95. row_dict['modified_image_path'] = settings.get_full_url(row_dict.get('modified_image_path'))
  96. return CardImageResponse.model_validate(dict(zip(columns, new_image_data)))
  97. except IntegrityError as e:
  98. db_conn.rollback()
  99. # 清理已上传到 MinIO 的文件
  100. try:
  101. minio_client.remove_object(settings.MINIO_BUCKET, object_name)
  102. except:
  103. pass
  104. if e.errno == 1062:
  105. raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在...")
  106. raise HTTPException(status_code=500, detail="数据库操作失败。")
  107. except Exception as e:
  108. db_conn.rollback()
  109. try:
  110. minio_client.remove_object(settings.MINIO_BUCKET, object_name)
  111. except:
  112. pass
  113. logger.error(f"关联图片到卡牌失败: {e}")
  114. if isinstance(e, HTTPException): raise e
  115. raise HTTPException(status_code=500, detail="数据库操作失败。")
  116. finally:
  117. if cursor: cursor.close()
  118. @router.post("/insert/gray/{card_id}", response_model=CardImageResponse, status_code=201,
  119. summary="[NEW] 为卡牌上传辅助图(无JSON)")
  120. async def upload_gray_image_for_card(
  121. card_id: int = Path(..., description="要关联的卡牌ID"),
  122. image_type: ImageType = Form(..., description="图片类型 (gray / ring / stripe1..4 等无JSON图)"),
  123. image: UploadFile = File(..., description="辅助图片文件"),
  124. db_conn: PooledMySQLConnection = db_dependency
  125. ):
  126. """
  127. 上传无 JSON 的辅助图,例如 gray / ring / stripe1..4。
  128. 融合图带分数 JSON,请改用 /insert/{card_id}。
  129. """
  130. if image_type in [ImageType.front_fusion, ImageType.back_fusion]:
  131. raise HTTPException(
  132. status_code=400,
  133. detail="融合图自带分数 JSON,请使用 /insert/{card_id} 接口上传"
  134. )
  135. # 1. 保存文件
  136. file_extension = os.path.splitext(image.filename)[1]
  137. unique_filename = f"gray_{uuid.uuid4()}{file_extension}" # 加个前缀区分
  138. relative_path = f"/Data/{unique_filename}"
  139. object_name = f"{settings.MINIO_BASE_PREFIX}{relative_path}"
  140. try:
  141. # 写入 MinIO 存储
  142. file_bytes = await image.read()
  143. minio_client.put_object(
  144. settings.MINIO_BUCKET,
  145. object_name,
  146. io.BytesIO(file_bytes),
  147. len(file_bytes),
  148. content_type=image.content_type or "image/jpeg"
  149. )
  150. except Exception as e:
  151. logger.error(f"保存图片到MinIO失败: {e}")
  152. raise HTTPException(status_code=500, detail="无法保存图片文件。")
  153. cursor = None
  154. try:
  155. cursor = db_conn.cursor()
  156. # 2. 检查卡牌存在
  157. cursor.execute(f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (card_id,))
  158. if not cursor.fetchone():
  159. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌不存在。")
  160. # 3. 插入数据 (card_gray_images 表)
  161. query_insert = (
  162. f"INSERT INTO {settings.DB_GRAY_IMAGE_TABLE_NAME} "
  163. "(card_id, image_type, image_path) VALUES (%s, %s, %s)"
  164. )
  165. cursor.execute(query_insert, (card_id, image_type.value, relative_path))
  166. new_id = cursor.lastrowid
  167. db_conn.commit()
  168. logger.info(f"灰度图 {new_id} 已关联到卡牌 {card_id}, 类型 {image_type.value}")
  169. # 4. 构造返回 (模拟 CardImageResponse 结构)
  170. # 因为数据库里只有基础字段,这里要补全 Pydantic 模型需要的字段
  171. cursor.execute(f"SELECT * FROM {settings.DB_GRAY_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,))
  172. new_data = cursor.fetchone() # tuple
  173. # 获取列名
  174. columns = [desc[0] for desc in cursor.description]
  175. row_dict = dict(zip(columns, new_data))
  176. # 补全空字段以符合 CardImageResponse
  177. from app.crud.crud_card import EMPTY_DETECTION_JSON
  178. response_dict = {
  179. **row_dict,
  180. "image_path": settings.get_full_url(row_dict.get('image_path')),
  181. "detection_json": EMPTY_DETECTION_JSON, # 默认死值
  182. "modified_json": None, # 刚上传还没有 modified
  183. "image_name": None,
  184. "detection_image_path": None,
  185. "modified_image_path": None,
  186. "is_edited": False
  187. }
  188. return CardImageResponse.model_validate(response_dict)
  189. except IntegrityError as e:
  190. db_conn.rollback()
  191. # 清理已上传到 MinIO 的文件
  192. try:
  193. minio_client.remove_object(settings.MINIO_BUCKET, object_name)
  194. except:
  195. pass
  196. if e.errno == 1062:
  197. raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在...")
  198. raise HTTPException(status_code=500, detail="数据库操作失败。")
  199. except Exception as e:
  200. db_conn.rollback()
  201. try:
  202. minio_client.remove_object(settings.MINIO_BUCKET, object_name)
  203. except:
  204. pass
  205. logger.error(f"关联图片到卡牌失败: {e}")
  206. if isinstance(e, HTTPException): raise e
  207. raise HTTPException(status_code=500, detail="数据库操作失败。")
  208. finally:
  209. if cursor: cursor.close()
  210. @router.get("/jsons/{id} [用户调用]", response_model=ImageJsonPairResponse, summary="获取图片的原始和修改后JSON")
  211. def get_image_jsons(id: int, db_conn: PooledMySQLConnection = db_dependency):
  212. # 此接口主要用于主图,灰度图没有实体的 JSON 存储,暂不支持直接通过此 ID 查询 JSON
  213. # 如果前端通过 format_xy 里的 query 接口获取,已经能在 list 里拿到了。
  214. cursor = None
  215. try:
  216. cursor = db_conn.cursor(dictionary=True)
  217. query = f"SELECT id, detection_json, modified_json FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s"
  218. cursor.execute(query, (id,))
  219. result = cursor.fetchone()
  220. if not result:
  221. raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到 (仅支持查询主图)。")
  222. return ImageJsonPairResponse.model_validate(result)
  223. except Exception as e:
  224. logger.error(f"获取JSON对失败 ({id}): {e}")
  225. if isinstance(e, HTTPException): raise e
  226. raise HTTPException(status_code=500, detail="数据库查询失败。")
  227. finally:
  228. if cursor: cursor.close()