cards.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import os
  2. from datetime import datetime
  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.scheme import CardDetailResponse, CardImageResponse, CardListDetailResponse
  10. logger = get_logger(__name__)
  11. router = APIRouter()
  12. db_dependency = Depends(get_db_connection)
  13. @router.post("/created", response_model=CardDetailResponse, status_code=201, summary="创建一个新的卡牌记录")
  14. def create_card(
  15. card_name: Optional[str] = Query(None, summary="卡牌的名称"),
  16. db_conn: PooledMySQLConnection = db_dependency
  17. ):
  18. """创建一个新的卡牌实体,此时它不关联任何图片。"""
  19. cursor = None
  20. try:
  21. cursor = db_conn.cursor()
  22. query = f"INSERT INTO {settings.DB_CARD_TABLE_NAME} (card_name) VALUES (%s)"
  23. cursor.execute(query, (card_name,))
  24. db_conn.commit()
  25. new_card_id = cursor.lastrowid
  26. logger.info(f"新卡牌已创建, ID: {new_card_id}")
  27. # 返回刚创建的空卡牌信息
  28. return CardDetailResponse(
  29. card_id=new_card_id,
  30. card_name=card_name,
  31. created_at=datetime.now(), # 模拟值
  32. updated_at=datetime.now(), # 模拟值
  33. images=[]
  34. )
  35. except Exception as e:
  36. db_conn.rollback()
  37. logger.error(f"创建卡牌失败: {e}")
  38. raise HTTPException(status_code=500, detail="数据库插入失败。")
  39. finally:
  40. if cursor:
  41. cursor.close()
  42. @router.get("/query/{card_id}", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息")
  43. def get_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
  44. """获取卡牌元数据以及所有与之关联的图片信息。"""
  45. cursor = None
  46. try:
  47. cursor = db_conn.cursor(dictionary=True) # 使用字典游标方便映射
  48. # 1. 获取卡牌信息
  49. query_card = f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} WHERE card_id = %s"
  50. cursor.execute(query_card, (card_id,))
  51. card_data = cursor.fetchone()
  52. if not card_data:
  53. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌未找到。")
  54. # 2. 获取所有关联的图片ID
  55. image_ids = [
  56. card_data['front_face_id'], card_data['back_face_id'],
  57. card_data['front_edge_id'], card_data['back_edge_id']
  58. ]
  59. # 过滤掉 NULL 值
  60. valid_image_ids = [img_id for img_id in image_ids if img_id is not None]
  61. images = []
  62. if valid_image_ids:
  63. # 使用 IN 子句一次性查询所有图片
  64. format_strings = ','.join(['%s'] * len(valid_image_ids))
  65. query_images = f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE image_id IN ({format_strings})"
  66. cursor.execute(query_images, tuple(valid_image_ids))
  67. image_records = cursor.fetchall()
  68. images = [CardImageResponse.model_validate(row) for row in image_records]
  69. # 组合成最终响应
  70. card_response = CardDetailResponse.model_validate(card_data)
  71. card_response.images = images
  72. return card_response
  73. except Exception as e:
  74. logger.error(f"查询卡牌详情失败 ({card_id}): {e}")
  75. if isinstance(e, HTTPException): raise e
  76. raise HTTPException(status_code=500, detail="数据库查询失败。")
  77. finally:
  78. if cursor:
  79. cursor.close()
  80. @router.get("/card_list", response_model=List[CardListDetailResponse], summary="获取卡牌列表")
  81. def list_cards_detailed(
  82. start_id: Optional[int] = Query(None, description="筛选条件:起始 card_id"),
  83. end_id: Optional[int] = Query(None, description="筛选条件:结束 card_id"),
  84. skip: int = Query(0, ge=0, description="分页:跳过的记录数"),
  85. limit: int = Query(100, ge=1, le=1000, description="分页:每页的记录数"),
  86. db_conn: PooledMySQLConnection = db_dependency
  87. ):
  88. """
  89. 获取卡牌的详细列表,支持按 card_id 范围筛选,并返回每张卡牌关联的图片ID和名称。
  90. """
  91. cursor = None
  92. try:
  93. cursor = db_conn.cursor(dictionary=True)
  94. # 基础查询语句,使用 LEFT JOIN 连接四次 images 表
  95. # 每次连接都用一个别名 (img_ff, img_bf, ...) 来区分
  96. base_query = f"""
  97. SELECT
  98. c.card_id,
  99. c.card_name,
  100. c.created_at,
  101. c.updated_at,
  102. img_ff.image_id AS front_face_id,
  103. img_ff.image_name AS front_face_name,
  104. img_bf.image_id AS back_face_id,
  105. img_bf.image_name AS back_face_name,
  106. img_fe.image_id AS front_edge_id,
  107. img_fe.image_name AS front_edge_name,
  108. img_be.image_id AS back_edge_id,
  109. img_be.image_name AS back_edge_name
  110. FROM
  111. {settings.DB_CARD_TABLE_NAME} AS c
  112. LEFT JOIN {settings.DB_IMAGE_TABLE_NAME} AS img_ff ON c.front_face_id = img_ff.image_id
  113. LEFT JOIN {settings.DB_IMAGE_TABLE_NAME} AS img_bf ON c.back_face_id = img_bf.image_id
  114. LEFT JOIN {settings.DB_IMAGE_TABLE_NAME} AS img_fe ON c.front_edge_id = img_fe.image_id
  115. LEFT JOIN {settings.DB_IMAGE_TABLE_NAME} AS img_be ON c.back_edge_id = img_be.image_id
  116. """
  117. # 动态构建 WHERE 条件
  118. conditions = []
  119. params = []
  120. if start_id is not None:
  121. conditions.append("c.card_id >= %s")
  122. params.append(start_id)
  123. if end_id is not None:
  124. conditions.append("c.card_id <= %s")
  125. params.append(end_id)
  126. if conditions:
  127. base_query += " WHERE " + " AND ".join(conditions)
  128. # 添加排序和分页
  129. base_query += " ORDER BY c.card_id DESC LIMIT %s OFFSET %s"
  130. params.extend([limit, skip])
  131. cursor.execute(base_query, tuple(params))
  132. sql_results = cursor.fetchall()
  133. # 手动将扁平的SQL结果映射到嵌套的Pydantic模型
  134. response_list = []
  135. for row in sql_results:
  136. card_data = {
  137. "card_id": row["card_id"],
  138. "card_name": row["card_name"],
  139. "created_at": row["created_at"],
  140. "updated_at": row["updated_at"],
  141. }
  142. # 检查并组装 front_face 数据
  143. if row["front_face_id"]:
  144. card_data["front_face"] = {"image_id": row["front_face_id"], "image_name": row["front_face_name"]}
  145. # 检查并组装 back_face 数据
  146. if row["back_face_id"]:
  147. card_data["back_face"] = {"image_id": row["back_face_id"], "image_name": row["back_face_name"]}
  148. # 检查并组装 front_edge 数据
  149. if row["front_edge_id"]:
  150. card_data["front_edge"] = {"image_id": row["front_edge_id"], "image_name": row["front_edge_name"]}
  151. # 检查并组装 back_edge 数据
  152. if row["back_edge_id"]:
  153. card_data["back_edge"] = {"image_id": row["back_edge_id"], "image_name": row["back_edge_name"]}
  154. response_list.append(CardListDetailResponse.model_validate(card_data))
  155. return response_list
  156. except Exception as e:
  157. logger.error(f"查询卡牌列表失败: {e}")
  158. raise HTTPException(status_code=500, detail="获取数据列表失败。")
  159. finally:
  160. if cursor:
  161. cursor.close()
  162. @router.delete("/delete/{card_id}", status_code=200, summary="删除卡牌及其所有关联图片")
  163. def delete_card(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
  164. """
  165. 删除一张卡牌。由于外键约束ON DELETE SET NULL,仅删除卡牌记录。
  166. 如果需要同时删除图片,需要先查询图片并手动删除。
  167. 我们将实现级联删除图片文件。
  168. """
  169. cursor = None
  170. try:
  171. cursor = db_conn.cursor()
  172. # 1. 查询所有关联图片的路径
  173. query_paths = f"""
  174. SELECT i.image_path
  175. FROM {settings.DB_CARD_TABLE_NAME} c
  176. JOIN {settings.DB_IMAGE_TABLE_NAME} i ON i.image_id IN
  177. (c.front_face_id, c.back_face_id, c.front_edge_id, c.back_edge_id)
  178. WHERE c.card_id = %s
  179. """
  180. cursor.execute(query_paths, (card_id,))
  181. image_paths = [row[0] for row in cursor.fetchall()]
  182. # 2. 删除卡牌记录 (这将触发删除图片记录,因为我们下面的逻辑会删除图片)
  183. # 注意: 如果直接删除 card, 外键设为 SET NULL, 图片记录不会被删。所以我们先删图片,再删card
  184. # 3. 删除图片记录
  185. if image_paths:
  186. format_strings = ','.join(['%s'] * len(image_paths))
  187. query_delete_images = f"DELETE FROM {settings.DB_IMAGE_TABLE_NAME} WHERE image_path IN ({format_strings})"
  188. cursor.execute(query_delete_images, tuple(image_paths))
  189. # 4. 删除卡牌记录
  190. query_delete_card = f"DELETE FROM {settings.DB_CARD_TABLE_NAME} WHERE card_id = %s"
  191. cursor.execute(query_delete_card, (card_id,))
  192. if cursor.rowcount == 0:
  193. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌未找到。")
  194. # 5. 删除物理文件
  195. for path in image_paths:
  196. if os.path.exists(path):
  197. os.remove(path)
  198. logger.info(f"图片文件已删除: {path}")
  199. db_conn.commit()
  200. logger.info(f"ID {card_id} 的卡牌和关联文件已成功删除。")
  201. return {"message": f"成功删除卡牌 ID {card_id} 及其所有关联数据"}
  202. except Exception as e:
  203. db_conn.rollback()
  204. logger.error(f"删除卡牌失败 ({card_id}): {e}")
  205. if isinstance(e, HTTPException): raise e
  206. raise HTTPException(status_code=500, detail="删除卡牌失败。")
  207. finally:
  208. if cursor:
  209. cursor.close()