formate_xy.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import requests
  2. import json
  3. from enum import Enum
  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. class QueryMode(str, Enum):
  20. current = "current"
  21. next = "next"
  22. prev = "prev"
  23. def _process_images_to_xy_format(card_data: dict):
  24. """
  25. 内部辅助函数:遍历卡牌数据中的图片,将 JSON 格式转换为前端需要的 XY 格式。
  26. 直接修改传入的 card_data 字典。
  27. """
  28. if "images" in card_data and card_data["images"]:
  29. for img in card_data["images"]:
  30. # 处理 detection_json
  31. if img.detection_json:
  32. d_json = img.detection_json
  33. if isinstance(d_json, str):
  34. d_json = json.loads(d_json)
  35. # *** 转换逻辑 ***
  36. img.detection_json = convert_internal_to_xy_format(d_json)
  37. # 处理 modified_json
  38. if img.modified_json:
  39. m_json = img.modified_json
  40. if isinstance(m_json, str):
  41. m_json = json.loads(m_json)
  42. # *** 转换逻辑 ***
  43. img.modified_json = convert_internal_to_xy_format(m_json)
  44. return card_data
  45. @router.get("/query", response_model=CardDetailResponse, summary="获取卡牌详细信息(格式化xy), 支持前后翻页")
  46. def get_card_details(
  47. card_id: int = Query(..., description="基准卡牌ID"),
  48. mode: QueryMode = Query(QueryMode.current, description="查询模式: current(当前), next(下一个), prev(上一个)"),
  49. db_conn: PooledMySQLConnection = db_dependency
  50. ):
  51. """
  52. 获取卡牌元数据以及所有与之关联的图片信息,并将坐标转换为 xy 格式。
  53. - **current**: 查询 card_id 对应的卡牌。
  54. - **next**: 查询 ID 比 card_id 大的第一张卡牌。
  55. - **prev**: 查询 ID 比 card_id 小的第一张卡牌。
  56. """
  57. target_id = card_id
  58. # 1. 如果是查询上一个或下一个,先计算目标ID
  59. if mode != QueryMode.current:
  60. try:
  61. with db_conn.cursor(dictionary=True) as cursor:
  62. if mode == QueryMode.next:
  63. query = (f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} "
  64. f"WHERE id > %s ORDER BY id ASC LIMIT 1")
  65. else: # mode == QueryMode.prev
  66. query = (f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} "
  67. f"WHERE id < %s ORDER BY id DESC LIMIT 1")
  68. cursor.execute(query, (card_id,))
  69. row = cursor.fetchone()
  70. if not row:
  71. # 按照项目习惯,找不到上/下一个时返回 200 + 提示信息,或根据需求改为 404
  72. msg = "没有下一张" if mode == QueryMode.next else "没有上一张"
  73. raise HTTPException(status_code=200, detail=msg)
  74. target_id = row['id']
  75. except HTTPException:
  76. raise
  77. except Exception as e:
  78. logger.error(f"查询卡牌ID失败 (Mode: {mode}, BaseID: {card_id}): {e}")
  79. raise HTTPException(status_code=500, detail="数据库查询失败")
  80. # 2. 获取目标卡牌的详细数据 (Dict 格式)
  81. card_data = crud_card.get_card_with_details(db_conn, target_id)
  82. if not card_data:
  83. raise HTTPException(status_code=404, detail=f"ID为 {target_id} 的卡牌未找到。")
  84. # 3. 遍历图片,转换格式 (使用抽取出的辅助函数)
  85. _process_images_to_xy_format(card_data)
  86. # 4. 验证并返回
  87. return CardDetailResponse.model_validate(card_data)
  88. @router.put("/update/json/{id}", status_code=200, summary="接收xy格式, 还原后重计算分数并保存")
  89. async def update_image_modified_json(
  90. id: int,
  91. new_json_data: dict = Body(..., description="前端传来的包含xy对象格式的JSON"),
  92. db_conn: PooledMySQLConnection = db_dependency
  93. ):
  94. """
  95. 接收前端传来的特殊格式 JSON (points 为对象列表)。
  96. 1. 将格式还原为后端标准格式 (points 为 [[x,y]])。
  97. 2. 根据 id 获取 image_type。
  98. 3. 调用外部接口重新计算分数。
  99. 4. 更新 modified_json。
  100. """
  101. card_id_to_update = None
  102. cursor = None
  103. # *** 1. 格式还原 ***
  104. # 将前端的 xy dict 格式转回 [[x,y]],并丢弃 points 里的 id
  105. internal_json_payload = convert_xy_to_internal_format(new_json_data)
  106. try:
  107. cursor = db_conn.cursor(dictionary=True)
  108. # 2. 获取图片信息
  109. cursor.execute(f"SELECT image_type, card_id FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (id,))
  110. row = cursor.fetchone()
  111. if not row:
  112. raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
  113. card_id_to_update = row["card_id"]
  114. image_type = row["image_type"]
  115. score_type = IMAGE_TYPE_TO_SCORE_TYPE.get(image_type)
  116. if not score_type:
  117. raise HTTPException(status_code=400, detail=f"未知的 image_type: {image_type}")
  118. logger.info(f"开始计算分数 (ID: {id}, Type: {score_type})")
  119. # 3. 调用远程计算接口 (使用还原后的 JSON)
  120. try:
  121. response = await run_in_threadpool(
  122. lambda: requests.post(
  123. settings.SCORE_RECALCULATE_ENDPOINT,
  124. params={"score_type": score_type},
  125. json=internal_json_payload, # 传递还原后的数据
  126. timeout=20
  127. )
  128. )
  129. except Exception as e:
  130. raise HTTPException(status_code=500, detail=f"调用分数计算服务失败: {e}")
  131. if response.status_code != 200:
  132. logger.error(f"分数计算接口返回错误: {response.status_code}, {response.text}")
  133. raise HTTPException(status_code=response.status_code,
  134. detail=f"分数计算接口返回错误: {response.text}")
  135. logger.info("分数计算完成")
  136. # 获取计算服务返回的结果(这个结果通常已经是标准的 internal 格式,带有分数和面积)
  137. final_json_data = response.json()
  138. # 4. 保存结果到数据库
  139. recalculated_json_str = json.dumps(final_json_data, ensure_ascii=False)
  140. update_query = (f"UPDATE {settings.DB_IMAGE_TABLE_NAME} "
  141. f"SET modified_json = %s, is_edited = TRUE "
  142. f"WHERE id = %s")
  143. cursor.execute(update_query, (recalculated_json_str, id))
  144. db_conn.commit()
  145. logger.info(f"图片ID {id} 的 modified_json 已更新并重新计算。")
  146. # 更新对应的 cards 的分数状态
  147. try:
  148. crud_card.update_card_scores_and_status(db_conn, card_id_to_update)
  149. logger.info(f"卡牌 {card_id_to_update} 的分数和状态已更新。")
  150. except Exception as score_update_e:
  151. logger.error(f"更新卡牌 {card_id_to_update} 分数失败: {score_update_e}")
  152. return {
  153. "detail": f"成功更新图片ID {id} 的JSON数据",
  154. "image_type": image_type,
  155. "score_type": score_type
  156. }
  157. except HTTPException:
  158. db_conn.rollback()
  159. raise
  160. except Exception as e:
  161. db_conn.rollback()
  162. logger.error(f"更新JSON失败 ({id}): {e}")
  163. raise HTTPException(status_code=500, detail=f"更新JSON数据失败: {e}")
  164. finally:
  165. if cursor:
  166. cursor.close()