cards.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. from datetime import datetime, date
  2. import os
  3. from typing import Optional, List
  4. from fastapi import APIRouter, Depends, HTTPException, Query
  5. from app.core.minio_client import minio_client
  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, CardListDetailResponse, CardType, SortBy,
  12. SortOrder, CardListResponseWrapper, CardListWithTotal, ReviewUpdate
  13. )
  14. from app.crud import crud_card
  15. logger = get_logger(__name__)
  16. router = APIRouter()
  17. db_dependency = Depends(get_db_connection)
  18. @router.post("/created", response_model=CardDetailResponse, status_code=201, summary="创建一个新的卡牌记录")
  19. def create_card(
  20. card_name: Optional[str] = Query(None, summary="卡牌的名称"),
  21. cardNo: Optional[str] = Query(None, summary="卡牌编号"),
  22. card_type: CardType = Query(CardType.pokemon, summary="卡牌类型"),
  23. db_conn: PooledMySQLConnection = db_dependency
  24. ):
  25. """创建一个新的卡牌实体,此时它不关联任何图片。"""
  26. try:
  27. with db_conn.cursor(dictionary=True) as cursor:
  28. query = f"INSERT INTO {settings.DB_CARD_TABLE_NAME} (card_name, cardNo, card_type) VALUES (%s, %s, %s)"
  29. cursor.execute(query, (card_name, cardNo, card_type.value))
  30. db_conn.commit()
  31. new_id = cursor.lastrowid
  32. logger.info(f"新卡牌已创建, ID: {new_id}, 类型: {card_type.value}")
  33. # 返回刚创建的空卡牌信息
  34. cursor.execute(f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (new_id,))
  35. new_card_data = cursor.fetchone()
  36. response_data = {**new_card_data, "images": []}
  37. return CardDetailResponse.model_validate(response_data)
  38. except Exception as e:
  39. db_conn.rollback()
  40. logger.error(f"创建卡牌失败: {e}")
  41. raise HTTPException(status_code=500, detail="数据库插入失败。")
  42. @router.get("/query", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息")
  43. def get_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
  44. """获取卡牌元数据以及所有与之关联的图片信息,包含 id_prev 和 id_next。"""
  45. # 1. 获取基本信息
  46. card_data = crud_card.get_card_with_details(db_conn, card_id)
  47. if not card_data:
  48. raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌未找到。")
  49. # 2. 获取 id_prev 和 id_next
  50. try:
  51. with db_conn.cursor() as cursor:
  52. # 查询上一个ID (ID < current ORDER BY DESC LIMIT 1)
  53. sql_prev = f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id < %s ORDER BY id DESC LIMIT 1"
  54. cursor.execute(sql_prev, (card_id,))
  55. row_prev = cursor.fetchone()
  56. card_data['id_prev'] = row_prev[0] if row_prev else None
  57. # 查询下一个ID (ID > current ORDER BY ASC LIMIT 1)
  58. sql_next = f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE id > %s ORDER BY id ASC LIMIT 1"
  59. cursor.execute(sql_next, (card_id,))
  60. row_next = cursor.fetchone()
  61. card_data['id_next'] = row_next[0] if row_next else None
  62. except Exception as e:
  63. logger.error(f"获取卡牌 {card_id} 的前后ID失败: {e}")
  64. # 不中断主流程,可以设为 None
  65. card_data['id_prev'] = None
  66. card_data['id_next'] = None
  67. return CardDetailResponse.model_validate(card_data)
  68. @router.get("/query_next", response_model=CardDetailResponse, summary="获取指定卡牌id的下一个卡的详细信息")
  69. def get_next_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependency): # Renamed function
  70. """获取指定ID的下一张卡牌的元数据以及所有与之关联的图片信息。"""
  71. try:
  72. with db_conn.cursor(dictionary=True) as cursor:
  73. query_next_card = (
  74. f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} "
  75. f"WHERE id > %s ORDER BY id ASC LIMIT 1"
  76. )
  77. cursor.execute(query_next_card, (card_id,))
  78. next_card_row = cursor.fetchone()
  79. if not next_card_row:
  80. raise HTTPException(status_code=200, detail=f"没有下一张")
  81. next_card_id = next_card_row['id']
  82. # 获取单个卡牌的完整信息
  83. card_data = crud_card.get_card_with_details(db_conn, next_card_id)
  84. if not card_data:
  85. raise HTTPException(status_code=404, detail=f"下一个卡牌ID {next_card_id} 未找到详细信息。")
  86. return CardDetailResponse.model_validate(card_data)
  87. except Exception as e:
  88. logger.error(f"查询下一个卡牌详情失败 (基准ID: {card_id}): {e}")
  89. if isinstance(e, HTTPException): raise e
  90. raise HTTPException(status_code=500, detail="服务器内部错误,查询数据库失败。")
  91. @router.get("/card_list_filter", response_model=CardListResponseWrapper, summary="获取卡牌列表和总数")
  92. def card_list_filter(
  93. card_id: Optional[int] = Query(None, description="筛选:卡牌ID"),
  94. cardNo: Optional[str] = Query(None, description="筛选:卡牌编号"),
  95. card_name: Optional[str] = Query(None, description="筛选:卡牌名称"),
  96. card_type: Optional[CardType] = Query(None, description="筛选:卡牌类型"),
  97. is_edited: Optional[bool] = Query(None, description="筛选:是否已编辑"),
  98. review_state: Optional[int] = Query(None, ge=1, le=4,
  99. description="筛选:审核状态(1待复检, 2已复检, 3审核未通过, 4审核通过)"),
  100. min_detection_score: Optional[float] = Query(None, ge=0, le=10),
  101. max_detection_score: Optional[float] = Query(None, ge=0, le=10),
  102. min_modified_score: Optional[float] = Query(None, ge=0, le=10),
  103. max_modified_score: Optional[float] = Query(None, ge=0, le=10),
  104. created_start: Optional[date] = Query(None),
  105. created_end: Optional[date] = Query(None),
  106. updated_start: Optional[date] = Query(None),
  107. updated_end: Optional[date] = Query(None),
  108. sort_by: SortBy = Query(SortBy.updated_at),
  109. sort_order: SortOrder = Query(SortOrder.desc),
  110. skip: int = Query(0, ge=0),
  111. page_num: int = Query(None, ge=1),
  112. limit: int = Query(100, ge=1, le=1000),
  113. db_conn: PooledMySQLConnection = db_dependency
  114. ):
  115. """
  116. 获取卡牌列表,返回格式包含 total 和 list。
  117. 结构: { "data": { "total": 100, "list": [...] } }
  118. """
  119. if page_num is not None:
  120. skip = (page_num - 1) * limit
  121. try:
  122. result = crud_card.get_card_list_and_count(
  123. db_conn,
  124. card_id, cardNo, card_name, card_type, is_edited, review_state,
  125. min_detection_score, max_detection_score,
  126. min_modified_score, max_modified_score,
  127. created_start, created_end,
  128. updated_start, updated_end,
  129. sort_by, sort_order, skip, limit
  130. )
  131. # 组装返回数据,注意这里要进行 model_validate 转换 list 中的每一项
  132. # 1. 先把 list 里的字典转成 Pydantic 对象
  133. validated_list = [CardListDetailResponse.model_validate(c) for c in result['list']]
  134. # 2. 构造 data 部分
  135. data_content = CardListWithTotal(
  136. total=result['total'],
  137. list=validated_list
  138. )
  139. # 3. 构造最外层包装
  140. return CardListResponseWrapper(data=data_content)
  141. except Exception as e:
  142. logger.error(f"查询卡牌列表(带总数)失败: {e}")
  143. raise HTTPException(status_code=500, detail="获取数据列表失败。")
  144. @router.delete("/delete/{id}", status_code=200, summary="删除卡牌及其所有关联图片")
  145. def delete_card(id: int, db_conn: PooledMySQLConnection = db_dependency):
  146. """
  147. 删除一张卡牌及其所有关联的图片记录和物理文件。
  148. 利用了数据库的 ON DELETE CASCADE 特性。
  149. """
  150. try:
  151. with db_conn.cursor() as cursor:
  152. # 1. 查询所有关联图片的所有物理文件路径
  153. query_paths = (f"SELECT image_path, detection_image_path, modified_image_path "
  154. f"FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s")
  155. cursor.execute(query_paths, (id,))
  156. image_paths_to_delete = [path for row in cursor.fetchall() for path in row if path]
  157. # 2. 删除卡牌记录
  158. query_delete_card = f"DELETE FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s"
  159. cursor.execute(query_delete_card, (id,))
  160. if cursor.rowcount == 0:
  161. raise HTTPException(status_code=404, detail=f"ID为 {id} 的卡牌未找到。")
  162. db_conn.commit()
  163. logger.info(f"ID {id} 的卡牌和关联数据已成功删除。")
  164. # 3. 删除物理文件 (改为 MinIO 删除)
  165. for path in image_paths_to_delete:
  166. # path 通常形如 /Data/xxx.jpg 或者 /DefectImage/xxx.jpg
  167. object_name = f"{settings.MINIO_BASE_PREFIX}{path}"
  168. try:
  169. minio_client.remove_object(settings.MINIO_BUCKET, object_name)
  170. logger.info(f"图片文件已从MinIO删除: {object_name}")
  171. except Exception as e:
  172. logger.error(f"删除MinIO文件失败 {object_name}: {e}")
  173. return {"message": f"成功删除卡牌 ID {id} 及其所有关联数据"}
  174. except Exception as e:
  175. db_conn.rollback()
  176. logger.error(f"删除卡牌失败 ({id}): {e}")
  177. if isinstance(e, HTTPException): raise e
  178. raise HTTPException(status_code=500, detail="删除卡牌失败。")
  179. @router.put("/review_state/{id}", status_code=200, summary="修改卡牌的审核状态")
  180. def update_review_state(
  181. id: int,
  182. data: ReviewUpdate,
  183. db_conn: PooledMySQLConnection = db_dependency
  184. ):
  185. """
  186. 修改某张卡牌的审核状态。
  187. review_state 参数说明:
  188. - 1: 待复检 (默认状态)
  189. - 2: 已复检
  190. - 3: 审核未通过
  191. - 4: 审核通过
  192. """
  193. review_state = data.review_state
  194. try:
  195. with db_conn.cursor() as cursor:
  196. cursor.execute(f"SELECT 1 FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (id,))
  197. if not cursor.fetchone():
  198. raise HTTPException(status_code=404, detail=f"ID为 {id} 的卡牌未找到。")
  199. # 更新指定 card_id 的 review_state 字段
  200. query_update = f"UPDATE {settings.DB_CARD_TABLE_NAME} SET review_state = %s WHERE id = %s"
  201. cursor.execute(query_update, (review_state, id))
  202. db_conn.commit()
  203. logger.info(f"卡牌 ID {id} 的审核状态已成功修改为 {review_state}。")
  204. return {"message": f"成功修改卡牌 ID {id} 的审核状态为 {review_state}"}
  205. except Exception as e:
  206. db_conn.rollback()
  207. logger.error(f"修改卡牌 {id} 审核状态失败: {e}")
  208. if isinstance(e, HTTPException):
  209. raise e
  210. raise HTTPException(status_code=500, detail="修改审核状态失败,数据库操作错误。")