| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- import requests
- import json
- from fastapi import APIRouter, Depends, HTTPException, Query, Body
- from fastapi.concurrency import run_in_threadpool
- from mysql.connector.pooling import PooledMySQLConnection
- from app.core.config import settings
- from app.core.logger import get_logger
- from app.core.database_loader import get_db_connection
- from app.utils.scheme import (
- CardDetailResponse, IMAGE_TYPE_TO_SCORE_TYPE
- )
- from app.crud import crud_card
- # 导入新写的工具函数
- from app.utils.xy_process import convert_internal_to_xy_format, convert_xy_to_internal_format
- logger = get_logger(__name__)
- router = APIRouter()
- db_dependency = Depends(get_db_connection)
- def _process_images_to_xy_format(card_data: dict):
- """
- 内部辅助函数:遍历卡牌数据中的图片,将 JSON 格式转换为前端需要的 XY 格式。
- 直接修改传入的 card_data 字典。
- """
- if "images" in card_data and card_data["images"]:
- for img in card_data["images"]:
- # 处理 detection_json
- if img.detection_json:
- d_json = img.detection_json
- if isinstance(d_json, str):
- d_json = json.loads(d_json)
- # *** 转换逻辑 ***
- img.detection_json = convert_internal_to_xy_format(d_json)
- # 处理 modified_json
- if img.modified_json:
- m_json = img.modified_json
- if isinstance(m_json, str):
- m_json = json.loads(m_json)
- # *** 转换逻辑 ***
- img.modified_json = convert_internal_to_xy_format(m_json)
- return card_data
- @router.get("/query", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息(格式化xy)")
- def get_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
- """
- 获取卡牌元数据以及所有与之关联的图片信息。
- 【特殊处理】:会将 json 中的 points 坐标转换为 [{"id":..., "x":..., "y":...}] 格式。
- """
- # 1. 获取原始数据
- card_data = crud_card.get_card_with_details(db_conn, card_id)
- if not card_data:
- raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌未找到。")
- # 2. 转换格式 (提取为公共函数)
- _process_images_to_xy_format(card_data)
- # 3. 验证并返回
- return CardDetailResponse.model_validate(card_data)
- @router.get("/query_next", response_model=CardDetailResponse, summary="获取下一个卡牌的详细信息(格式化xy)")
- def get_next_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
- """
- 获取指定ID的下一张卡牌的信息,并格式化为xy坐标结构。
- """
- try:
- with db_conn.cursor(dictionary=True) as cursor:
- # 1. 查询下一个ID (ID > current ORDER BY ID ASC)
- query_next = (
- f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} "
- f"WHERE id > %s ORDER BY id ASC LIMIT 1"
- )
- cursor.execute(query_next, (card_id,))
- row = cursor.fetchone()
- if not row:
- # 保持与 cards.py 一致的行为,没有下一张时返回 200 或 404 视业务需求定,这里参考 cards.py 逻辑
- raise HTTPException(status_code=200, detail=f"没有下一张")
- next_card_id = row['id']
- # 2. 获取数据
- card_data = crud_card.get_card_with_details(db_conn, next_card_id)
- if not card_data:
- raise HTTPException(status_code=404, detail=f"下一个卡牌ID {next_card_id} 数据异常。")
- # 3. 转换格式
- _process_images_to_xy_format(card_data)
- return CardDetailResponse.model_validate(card_data)
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"查询下一个卡牌(xy)失败 (基准ID: {card_id}): {e}")
- raise HTTPException(status_code=500, detail="查询失败")
- @router.get("/query_prev", response_model=CardDetailResponse, summary="获取上一个卡牌的详细信息(格式化xy)")
- def get_prev_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
- """
- 获取指定ID的上一张卡牌的信息,并格式化为xy坐标结构。
- """
- try:
- with db_conn.cursor(dictionary=True) as cursor:
- # 1. 查询上一个ID (ID < current ORDER BY ID DESC)
- query_prev = (
- f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} "
- f"WHERE id < %s ORDER BY id DESC LIMIT 1"
- )
- cursor.execute(query_prev, (card_id,))
- row = cursor.fetchone()
- if not row:
- raise HTTPException(status_code=200, detail=f"没有上一张")
- prev_card_id = row['id']
- # 2. 获取数据
- card_data = crud_card.get_card_with_details(db_conn, prev_card_id)
- if not card_data:
- raise HTTPException(status_code=404, detail=f"上一个卡牌ID {prev_card_id} 数据异常。")
- # 3. 转换格式
- _process_images_to_xy_format(card_data)
- return CardDetailResponse.model_validate(card_data)
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"查询上一个卡牌(xy)失败 (基准ID: {card_id}): {e}")
- raise HTTPException(status_code=500, detail="查询失败")
- @router.put("/update/json/{id}", status_code=200, summary="接收xy格式, 还原后重计算分数并保存")
- async def update_image_modified_json(
- id: int,
- new_json_data: dict = Body(..., description="前端传来的包含xy对象格式的JSON"),
- db_conn: PooledMySQLConnection = db_dependency
- ):
- """
- 接收前端传来的特殊格式 JSON (points 为对象列表)。
- 1. 将格式还原为后端标准格式 (points 为 [[x,y]])。
- 2. 根据 id 获取 image_type。
- 3. 调用外部接口重新计算分数。
- 4. 更新 modified_json。
- """
- card_id_to_update = None
- cursor = None
- # *** 1. 格式还原 ***
- # 将前端的 xy dict 格式转回 [[x,y]],并丢弃 points 里的 id
- internal_json_payload = convert_xy_to_internal_format(new_json_data)
- try:
- cursor = db_conn.cursor(dictionary=True)
- # 2. 获取图片信息
- cursor.execute(f"SELECT image_type, card_id FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (id,))
- row = cursor.fetchone()
- if not row:
- raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
- card_id_to_update = row["card_id"]
- image_type = row["image_type"]
- score_type = IMAGE_TYPE_TO_SCORE_TYPE.get(image_type)
- if not score_type:
- raise HTTPException(status_code=400, detail=f"未知的 image_type: {image_type}")
- logger.info(f"开始计算分数 (ID: {id}, Type: {score_type})")
- # 3. 调用远程计算接口 (使用还原后的 JSON)
- try:
- response = await run_in_threadpool(
- lambda: requests.post(
- settings.SCORE_RECALCULATE_ENDPOINT,
- params={"score_type": score_type},
- json=internal_json_payload, # 传递还原后的数据
- timeout=20
- )
- )
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"调用分数计算服务失败: {e}")
- if response.status_code != 200:
- logger.error(f"分数计算接口返回错误: {response.status_code}, {response.text}")
- raise HTTPException(status_code=response.status_code,
- detail=f"分数计算接口返回错误: {response.text}")
- logger.info("分数计算完成")
- # 获取计算服务返回的结果(这个结果通常已经是标准的 internal 格式,带有分数和面积)
- final_json_data = response.json()
- # 4. 保存结果到数据库
- recalculated_json_str = json.dumps(final_json_data, ensure_ascii=False)
- update_query = (f"UPDATE {settings.DB_IMAGE_TABLE_NAME} "
- f"SET modified_json = %s, is_edited = TRUE "
- f"WHERE id = %s")
- cursor.execute(update_query, (recalculated_json_str, id))
- db_conn.commit()
- logger.info(f"图片ID {id} 的 modified_json 已更新并重新计算。")
- # 更新对应的 cards 的分数状态
- try:
- crud_card.update_card_scores_and_status(db_conn, card_id_to_update)
- logger.info(f"卡牌 {card_id_to_update} 的分数和状态已更新。")
- except Exception as score_update_e:
- logger.error(f"更新卡牌 {card_id_to_update} 分数失败: {score_update_e}")
- return {
- "detail": f"成功更新图片ID {id} 的JSON数据",
- "image_type": image_type,
- "score_type": score_type
- }
- except HTTPException:
- db_conn.rollback()
- raise
- except Exception as e:
- db_conn.rollback()
- logger.error(f"更新JSON失败 ({id}): {e}")
- raise HTTPException(status_code=500, detail=f"更新JSON数据失败: {e}")
- finally:
- if cursor:
- cursor.close()
|