formate_xy.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. # --- START OF FILE app/api/formate_xy.py ---
  2. import requests
  3. import json
  4. from fastapi import APIRouter, Depends, HTTPException, Query, Body
  5. from fastapi.concurrency import run_in_threadpool
  6. from mysql.connector.pooling import PooledMySQLConnection
  7. from app.core.config import settings
  8. from app.core.logger import get_logger
  9. from app.core.database_loader import get_db_connection
  10. from app.utils.scheme import (
  11. CardDetailResponse, IMAGE_TYPE_TO_SCORE_TYPE
  12. )
  13. from app.crud import crud_card
  14. # 导入新写的工具函数
  15. from app.utils.xy_process import convert_internal_to_xy_format, convert_xy_to_internal_format
  16. logger = get_logger(__name__)
  17. router = APIRouter()
  18. db_dependency = Depends(get_db_connection)
  19. @router.get("/query", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息(格式化xy)")
  20. def get_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
  21. """
  22. 获取卡牌元数据以及所有与之关联的图片信息。
  23. 【特殊处理】:会将 json 中的 points 坐标转换为 [{"id":..., "x":..., "y":...}] 格式,
  24. 并在 defects 列表中每项添加 id。
  25. """
  26. # 1. 获取原始数据 (Dict 格式)
  27. # 注意:这里直接获取 Dict,而不是让 Pydantic 马上校验,因为我们需要先修改数据结构
  28. card_data = crud_card.get_card_with_details(db_conn, card_id)
  29. if not card_data:
  30. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌未找到。")
  31. # 2. 遍历图片,转换格式
  32. # crud_card 返回的 card_data["images"] 是一个 list of dict
  33. if "images" in card_data and card_data["images"]:
  34. for img in card_data["images"]:
  35. # 处理 detection_json
  36. if img.detection_json:
  37. # 如果数据库取出的是字符串,先转 dict (通常 cursor dictionary=True 且 JSON 列会自动转,但在某些驱动下可能是 str)
  38. d_json = img.detection_json
  39. if isinstance(d_json, str):
  40. d_json = json.loads(d_json)
  41. # *** 转换逻辑 ***
  42. img.detection_json = convert_internal_to_xy_format(d_json)
  43. # 处理 modified_json
  44. if img.modified_json:
  45. m_json = img.modified_json
  46. if isinstance(m_json, str):
  47. m_json = json.loads(m_json)
  48. # *** 转换逻辑 ***
  49. img.modified_json = convert_internal_to_xy_format(m_json)
  50. # 3. 验证并返回
  51. # 因为 CardDetailResponse 定义中 detection_json 是 Dict[str, Any],
  52. # 所以无论里面是 [[x,y]] 还是 [{"x":x...}] 都可以通过校验
  53. return CardDetailResponse.model_validate(card_data)
  54. @router.put("/update/json/{id}", status_code=200, summary="接收xy格式, 还原后重计算分数并保存")
  55. async def update_image_modified_json(
  56. id: int,
  57. new_json_data: dict = Body(..., description="前端传来的包含xy对象格式的JSON"),
  58. db_conn: PooledMySQLConnection = db_dependency
  59. ):
  60. """
  61. 接收前端传来的特殊格式 JSON (points 为对象列表)。
  62. 1. 将格式还原为后端标准格式 (points 为 [[x,y]])。
  63. 2. 根据 id 获取 image_type。
  64. 3. 调用外部接口重新计算分数。
  65. 4. 更新 modified_json。
  66. """
  67. card_id_to_update = None
  68. cursor = None
  69. # *** 1. 格式还原 ***
  70. # 将前端的 xy dict 格式转回 [[x,y]],并丢弃 points 里的 id
  71. internal_json_payload = convert_xy_to_internal_format(new_json_data)
  72. try:
  73. cursor = db_conn.cursor(dictionary=True)
  74. # 2. 获取图片信息
  75. cursor.execute(f"SELECT image_type, card_id FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (id,))
  76. row = cursor.fetchone()
  77. if not row:
  78. raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
  79. card_id_to_update = row["card_id"]
  80. image_type = row["image_type"]
  81. score_type = IMAGE_TYPE_TO_SCORE_TYPE.get(image_type)
  82. if not score_type:
  83. raise HTTPException(status_code=400, detail=f"未知的 image_type: {image_type}")
  84. logger.info(f"开始计算分数 (ID: {id}, Type: {score_type})")
  85. # 3. 调用远程计算接口 (使用还原后的 JSON)
  86. try:
  87. response = await run_in_threadpool(
  88. lambda: requests.post(
  89. settings.SCORE_RECALCULATE_ENDPOINT,
  90. params={"score_type": score_type},
  91. json=internal_json_payload, # 传递还原后的数据
  92. timeout=20
  93. )
  94. )
  95. except Exception as e:
  96. raise HTTPException(status_code=500, detail=f"调用分数计算服务失败: {e}")
  97. if response.status_code != 200:
  98. logger.error(f"分数计算接口返回错误: {response.status_code}, {response.text}")
  99. raise HTTPException(status_code=response.status_code,
  100. detail=f"分数计算接口返回错误: {response.text}")
  101. logger.info("分数计算完成")
  102. # 获取计算服务返回的结果(这个结果通常已经是标准的 internal 格式,带有分数和面积)
  103. final_json_data = response.json()
  104. # 4. 保存结果到数据库
  105. recalculated_json_str = json.dumps(final_json_data, ensure_ascii=False)
  106. update_query = (f"UPDATE {settings.DB_IMAGE_TABLE_NAME} "
  107. f"SET modified_json = %s, is_edited = TRUE "
  108. f"WHERE id = %s")
  109. cursor.execute(update_query, (recalculated_json_str, id))
  110. db_conn.commit()
  111. logger.info(f"图片ID {id} 的 modified_json 已更新并重新计算。")
  112. # 更新对应的 cards 的分数状态
  113. try:
  114. crud_card.update_card_scores_and_status(db_conn, card_id_to_update)
  115. logger.info(f"卡牌 {card_id_to_update} 的分数和状态已更新。")
  116. except Exception as score_update_e:
  117. logger.error(f"更新卡牌 {card_id_to_update} 分数失败: {score_update_e}")
  118. return {
  119. "message": f"成功更新图片ID {id} 的JSON数据",
  120. "image_type": image_type,
  121. "score_type": score_type
  122. }
  123. except HTTPException:
  124. db_conn.rollback()
  125. raise
  126. except Exception as e:
  127. db_conn.rollback()
  128. logger.error(f"更新JSON失败 ({id}): {e}")
  129. raise HTTPException(status_code=500, detail=f"更新JSON数据失败: {e}")
  130. finally:
  131. if cursor:
  132. cursor.close()