cards.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. from datetime import datetime
  2. import os
  3. from typing import Optional, List
  4. from fastapi import APIRouter, Depends, HTTPException, Query
  5. from mysql.connector.pooling import PooledMySQLConnection
  6. from app.core.config import settings
  7. from app.core.logger import get_logger
  8. from app.core.database_loader import get_db_connection
  9. from app.utils.card_score_calculate import card_score_calculate
  10. from app.utils.scheme import CardDetailResponse, CardImageResponse, CardListDetailResponse
  11. logger = get_logger(__name__)
  12. router = APIRouter()
  13. db_dependency = Depends(get_db_connection)
  14. @router.post("/created", response_model=CardDetailResponse, status_code=201, summary="创建一个新的卡牌记录")
  15. def create_card(
  16. card_name: Optional[str] = Query(None, summary="卡牌的名称"),
  17. db_conn: PooledMySQLConnection = db_dependency
  18. ):
  19. """创建一个新的卡牌实体,此时它不关联任何图片。"""
  20. cursor = None
  21. try:
  22. cursor = db_conn.cursor()
  23. query = f"INSERT INTO {settings.DB_CARD_TABLE_NAME} (card_name) VALUES (%s)"
  24. cursor.execute(query, (card_name,))
  25. db_conn.commit()
  26. new_id = cursor.lastrowid
  27. logger.info(f"新卡牌已创建, ID: {new_id}")
  28. # 返回刚创建的空卡牌信息
  29. return CardDetailResponse(
  30. id=new_id,
  31. card_name=card_name,
  32. created_at=datetime.now(),
  33. updated_at=datetime.now(),
  34. images=[]
  35. )
  36. except Exception as e:
  37. db_conn.rollback()
  38. logger.error(f"创建卡牌失败: {e}")
  39. raise HTTPException(status_code=500, detail="数据库插入失败。")
  40. finally:
  41. if cursor:
  42. cursor.close()
  43. @router.get("/query/{id}", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息")
  44. def get_card_details(id: int, db_conn: PooledMySQLConnection = db_dependency):
  45. """获取卡牌元数据以及所有与之关联的图片信息。"""
  46. cursor = None
  47. try:
  48. cursor = db_conn.cursor(dictionary=True)
  49. # 1. 获取卡牌信息
  50. query_card = f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s"
  51. cursor.execute(query_card, (id,))
  52. card_data = cursor.fetchone()
  53. if not card_data:
  54. raise HTTPException(status_code=404, detail=f"ID为 {id} 的卡牌未找到。")
  55. # 2. 获取所有关联的图片信息
  56. query_images = f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s"
  57. cursor.execute(query_images, (id,))
  58. image_records = cursor.fetchall()
  59. images = [CardImageResponse.model_validate(row) for row in image_records]
  60. # 计算总分数(只有当图片数量为 4 时才计算)
  61. card_response = card_score_calculate(card_data, images)
  62. return card_response
  63. except Exception as e:
  64. logger.error(f"查询卡牌详情失败 ({id}): {e}")
  65. if isinstance(e, HTTPException): raise e
  66. raise HTTPException(status_code=500, detail="数据库查询失败。")
  67. finally:
  68. if cursor:
  69. cursor.close()
  70. @router.get("/query_next/{id}", response_model=CardDetailResponse, summary="获取指定卡牌id的下一个卡的详细信息")
  71. def get_card_details(id: int, db_conn: PooledMySQLConnection = db_dependency):
  72. """获取指定ID的下一张卡牌的元数据以及所有与之关联的图片信息。"""
  73. try:
  74. with db_conn.cursor(dictionary=True) as cursor:
  75. # 1. 获取下一张卡牌的
  76. query_next_card = (
  77. f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} "
  78. f"WHERE id > %s ORDER BY id ASC LIMIT 1"
  79. )
  80. cursor.execute(query_next_card, (id,))
  81. next_card_data = cursor.fetchone()
  82. # 如果没有找到下一张卡牌,则抛出 404 错误
  83. if not next_card_data:
  84. raise HTTPException(status_code=404, detail=f"ID为 {id} 的下一个卡牌未找到。")
  85. # 从获取到的下一张卡牌数据中提取其ID
  86. next_card_id = next_card_data['id']
  87. # 2. 使用【下一个卡牌的ID】来获取所有关联的图片信息
  88. query_images = f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s"
  89. cursor.execute(query_images, (next_card_id,)) # <-- 关键修正:使用 next_card_id
  90. image_records = cursor.fetchall()
  91. images = [CardImageResponse.model_validate(row) for row in image_records]
  92. # 3. 使用【正确的下一张卡牌数据】和【正确的图片数据】进行计算
  93. card_response = card_score_calculate(next_card_data, images) # <-- 关键修正:传入 next_card_data
  94. return card_response
  95. except Exception as e:
  96. logger.error(f"查询下一个卡牌详情失败 (基准ID: {id}): {e}")
  97. # 如果异常已经是 HTTPException,直接重新抛出,否则包装成 500 错误
  98. if isinstance(e, HTTPException):
  99. raise e
  100. raise HTTPException(status_code=500, detail="服务器内部错误,查询数据库失败。")
  101. @router.get("/card_list", response_model=List[CardListDetailResponse], summary="获取卡牌列表")
  102. def list_cards_detailed(
  103. start_id: Optional[int] = Query(None, description="筛选条件:起始 card_id"),
  104. end_id: Optional[int] = Query(None, description="筛选条件:结束 card_id"),
  105. skip: int = Query(0, ge=0, description="分页:跳过的记录数"),
  106. limit: int = Query(100, ge=1, le=1000, description="分页:每页的记录数"),
  107. db_conn: PooledMySQLConnection = db_dependency
  108. ):
  109. """
  110. 获取卡牌的基础信息列表,支持按 id 范围筛选和分页。
  111. """
  112. cursor = None
  113. try:
  114. cursor = db_conn.cursor(dictionary=True)
  115. base_query = f"SELECT id, card_name, created_at, updated_at FROM {settings.DB_CARD_TABLE_NAME}"
  116. conditions = []
  117. params = []
  118. if start_id is not None:
  119. conditions.append("id >= %s")
  120. params.append(start_id)
  121. if end_id is not None:
  122. conditions.append("id <= %s")
  123. params.append(end_id)
  124. if conditions:
  125. base_query += " WHERE " + " AND ".join(conditions)
  126. base_query += " ORDER BY id DESC LIMIT %s OFFSET %s"
  127. params.extend([limit, skip])
  128. cursor.execute(base_query, tuple(params))
  129. results = cursor.fetchall()
  130. return [CardListDetailResponse.model_validate(row) for row in results]
  131. except Exception as e:
  132. logger.error(f"查询卡牌列表失败: {e}")
  133. raise HTTPException(status_code=500, detail="获取数据列表失败。")
  134. finally:
  135. if cursor:
  136. cursor.close()
  137. @router.delete("/delete/{id}", status_code=200, summary="删除卡牌及其所有关联图片")
  138. def delete_card(id: int, db_conn: PooledMySQLConnection = db_dependency):
  139. """
  140. 删除一张卡牌及其所有关联的图片记录和物理文件。
  141. 利用了数据库的 ON DELETE CASCADE 特性。
  142. """
  143. cursor = None
  144. try:
  145. cursor = db_conn.cursor()
  146. # 1. 查询所有关联图片的所有物理文件路径,以便稍后删除 (修改部分)
  147. query_paths = (f"SELECT image_path, detection_image_path, modified_image_path "
  148. f"FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s")
  149. cursor.execute(query_paths, (id,))
  150. image_paths_to_delete = []
  151. for row in cursor.fetchall():
  152. # 将每一行中非空的路径都添加到待删除列表
  153. image_paths_to_delete.extend([path for path in row if path])
  154. # 2. 删除卡牌记录。数据库会自动级联删除 card_images 表中的相关记录
  155. query_delete_card = f"DELETE FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s"
  156. cursor.execute(query_delete_card, (id,))
  157. if cursor.rowcount == 0:
  158. raise HTTPException(status_code=404, detail=f"ID为 {id} 的卡牌未找到。")
  159. # 3. 删除物理文件
  160. for path in image_paths_to_delete:
  161. absolute_path = settings.BASE_PATH / path.lstrip('/\\')
  162. if os.path.exists(absolute_path):
  163. try:
  164. os.remove(absolute_path)
  165. logger.info(f"图片文件已删除: {absolute_path}")
  166. except OSError as e:
  167. logger.error(f"删除文件失败 {absolute_path}: {e}")
  168. db_conn.commit()
  169. logger.info(f"ID {id} 的卡牌和关联数据已成功删除。")
  170. return {"message": f"成功删除卡牌 ID {id} 及其所有关联数据"}
  171. except Exception as e:
  172. db_conn.rollback()
  173. logger.error(f"删除卡牌失败 ({id}): {e}")
  174. if isinstance(e, HTTPException): raise e
  175. raise HTTPException(status_code=500, detail="删除卡牌失败。")
  176. finally:
  177. if cursor:
  178. cursor.close()