formate_xy.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. 同时返回上一张和下一张卡牌的ID。
  54. - **current**: 查询 card_id 对应的卡牌。
  55. - **next**: 查询 ID 比 card_id 大的第一张卡牌。
  56. - **prev**: 查询 ID 比 card_id 小的第一张卡牌。
  57. """
  58. target_id = card_id
  59. cursor = None
  60. try:
  61. cursor = db_conn.cursor(dictionary=True)
  62. # 1. 如果是查询上一个或下一个,先计算目标ID
  63. if mode != QueryMode.current:
  64. if mode == QueryMode.next:
  65. query_target = (f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} "
  66. f"WHERE id > %s ORDER BY id ASC LIMIT 1")
  67. else: # mode == QueryMode.prev
  68. query_target = (f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} "
  69. f"WHERE id < %s ORDER BY id DESC LIMIT 1")
  70. cursor.execute(query_target, (card_id,))
  71. row = cursor.fetchone()
  72. if not row:
  73. msg = "没有下一张" if mode == QueryMode.next else "没有上一张"
  74. raise HTTPException(status_code=200, detail=msg)
  75. target_id = row['id']
  76. # 2. 获取目标卡牌的详细数据 (Dict 格式)
  77. card_data = crud_card.get_card_with_details(db_conn, target_id)
  78. if not card_data:
  79. raise HTTPException(status_code=404, detail=f"ID为 {target_id} 的卡牌未找到。")
  80. # 3. 补充当前目标卡牌的 id_prev 和 id_next
  81. # 注意:这里需要重新获取 cursor,或者使用 cursor (非 dict 模式可能更方便取值,但 dict 模式也行)
  82. # 这里为了简单直接用 raw SQL
  83. # 查询上一个ID
  84. sql_prev = f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id < %s ORDER BY id DESC LIMIT 1"
  85. cursor.execute(sql_prev, (target_id,))
  86. row_prev = cursor.fetchone()
  87. card_data['id_prev'] = row_prev['id'] if row_prev else None
  88. # 查询下一个ID
  89. sql_next = f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id > %s ORDER BY id ASC LIMIT 1"
  90. cursor.execute(sql_next, (target_id,))
  91. row_next = cursor.fetchone()
  92. card_data['id_next'] = row_next['id'] if row_next else None
  93. # 4. 遍历图片,转换格式 (使用抽取出的辅助函数)
  94. _process_images_to_xy_format(card_data)
  95. # 5. 验证并返回
  96. return CardDetailResponse.model_validate(card_data)
  97. except HTTPException:
  98. raise
  99. except Exception as e:
  100. logger.error(f"查询卡牌详情失败 (Mode: {mode}, BaseID: {card_id}): {e}")
  101. raise HTTPException(status_code=500, detail="数据库查询失败")
  102. finally:
  103. if cursor:
  104. cursor.close()
  105. @router.put("/update/json/{id}", status_code=200, summary="接收xy格式, 还原后重计算分数并保存")
  106. async def update_image_modified_json(
  107. id: int,
  108. new_json_data: dict = Body(..., description="前端传来的包含xy对象格式的JSON"),
  109. db_conn: PooledMySQLConnection = db_dependency
  110. ):
  111. """
  112. 接收前端传来的特殊格式 JSON (points 为对象列表)。
  113. 1. 将格式还原为后端标准格式 (points 为 [[x,y]])。
  114. 2. 根据 id 获取 image_type。
  115. 3. 调用外部接口重新计算分数。
  116. 4. 更新 modified_json。
  117. """
  118. card_id_to_update = None
  119. cursor = None
  120. # *** 1. 格式还原 ***
  121. # 将前端的 xy dict 格式转回 [[x,y]],并丢弃 points 里的 id
  122. internal_json_payload = convert_xy_to_internal_format(new_json_data)
  123. try:
  124. cursor = db_conn.cursor(dictionary=True)
  125. # 2. 获取图片信息
  126. cursor.execute(f"SELECT image_type, card_id FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (id,))
  127. row = cursor.fetchone()
  128. if not row:
  129. raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
  130. card_id_to_update = row["card_id"]
  131. image_type = row["image_type"]
  132. score_type = IMAGE_TYPE_TO_SCORE_TYPE.get(image_type)
  133. if not score_type:
  134. raise HTTPException(status_code=400, detail=f"未知的 image_type: {image_type}")
  135. logger.info(f"开始计算分数 (ID: {id}, Type: {score_type})")
  136. # 3. 调用远程计算接口 (使用还原后的 JSON)
  137. try:
  138. response = await run_in_threadpool(
  139. lambda: requests.post(
  140. settings.SCORE_RECALCULATE_ENDPOINT,
  141. params={"score_type": score_type},
  142. json=internal_json_payload, # 传递还原后的数据
  143. timeout=20
  144. )
  145. )
  146. except Exception as e:
  147. raise HTTPException(status_code=500, detail=f"调用分数计算服务失败: {e}")
  148. if response.status_code != 200:
  149. logger.error(f"分数计算接口返回错误: {response.status_code}, {response.text}")
  150. raise HTTPException(status_code=response.status_code,
  151. detail=f"分数计算接口返回错误: {response.text}")
  152. logger.info("分数计算完成")
  153. # 获取计算服务返回的结果(这个结果通常已经是标准的 internal 格式,带有分数和面积)
  154. final_json_data = response.json()
  155. # 4. 保存结果到数据库
  156. recalculated_json_str = json.dumps(final_json_data, ensure_ascii=False)
  157. update_query = (f"UPDATE {settings.DB_IMAGE_TABLE_NAME} "
  158. f"SET modified_json = %s, is_edited = TRUE "
  159. f"WHERE id = %s")
  160. cursor.execute(update_query, (recalculated_json_str, id))
  161. db_conn.commit()
  162. logger.info(f"图片ID {id} 的 modified_json 已更新并重新计算。")
  163. # 更新对应的 cards 的分数状态
  164. try:
  165. crud_card.update_card_scores_and_status(db_conn, card_id_to_update)
  166. logger.info(f"卡牌 {card_id_to_update} 的分数和状态已更新。")
  167. except Exception as score_update_e:
  168. logger.error(f"更新卡牌 {card_id_to_update} 分数失败: {score_update_e}")
  169. return {
  170. "detail": f"成功更新图片ID {id} 的JSON数据",
  171. "image_type": image_type,
  172. "score_type": score_type
  173. }
  174. except HTTPException:
  175. db_conn.rollback()
  176. raise
  177. except Exception as e:
  178. db_conn.rollback()
  179. logger.error(f"更新JSON失败 ({id}): {e}")
  180. raise HTTPException(status_code=500, detail=f"更新JSON数据失败: {e}")
  181. finally:
  182. if cursor:
  183. cursor.close()