cards.py 12 KB

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