|
|
@@ -1,5 +1,5 @@
|
|
|
-import os
|
|
|
from datetime import datetime
|
|
|
+import os
|
|
|
from typing import Optional, List
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
from mysql.connector.pooling import PooledMySQLConnection
|
|
|
@@ -26,15 +26,15 @@ def create_card(
|
|
|
query = f"INSERT INTO {settings.DB_CARD_TABLE_NAME} (card_name) VALUES (%s)"
|
|
|
cursor.execute(query, (card_name,))
|
|
|
db_conn.commit()
|
|
|
- new_card_id = cursor.lastrowid
|
|
|
- logger.info(f"新卡牌已创建, ID: {new_card_id}")
|
|
|
+ new_id = cursor.lastrowid
|
|
|
+ logger.info(f"新卡牌已创建, ID: {new_id}")
|
|
|
|
|
|
# 返回刚创建的空卡牌信息
|
|
|
return CardDetailResponse(
|
|
|
- card_id=new_card_id,
|
|
|
+ id=new_id,
|
|
|
card_name=card_name,
|
|
|
- created_at=datetime.now(), # 模拟值
|
|
|
- updated_at=datetime.now(), # 模拟值
|
|
|
+ created_at=datetime.now(),
|
|
|
+ updated_at=datetime.now(),
|
|
|
images=[]
|
|
|
)
|
|
|
except Exception as e:
|
|
|
@@ -46,36 +46,25 @@ def create_card(
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
|
-@router.get("/query/{card_id}", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息")
|
|
|
-def get_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
|
|
|
+@router.get("/query/{id}", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息")
|
|
|
+def get_card_details(id: int, db_conn: PooledMySQLConnection = db_dependency):
|
|
|
"""获取卡牌元数据以及所有与之关联的图片信息。"""
|
|
|
cursor = None
|
|
|
try:
|
|
|
- cursor = db_conn.cursor(dictionary=True) # 使用字典游标方便映射
|
|
|
+ cursor = db_conn.cursor(dictionary=True)
|
|
|
|
|
|
# 1. 获取卡牌信息
|
|
|
- query_card = f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} WHERE card_id = %s"
|
|
|
- cursor.execute(query_card, (card_id,))
|
|
|
+ query_card = f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s"
|
|
|
+ cursor.execute(query_card, (id,))
|
|
|
card_data = cursor.fetchone()
|
|
|
if not card_data:
|
|
|
- raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌未找到。")
|
|
|
-
|
|
|
- # 2. 获取所有关联的图片ID
|
|
|
- image_ids = [
|
|
|
- card_data['front_face_id'], card_data['back_face_id'],
|
|
|
- card_data['front_edge_id'], card_data['back_edge_id']
|
|
|
- ]
|
|
|
- # 过滤掉 NULL 值
|
|
|
- valid_image_ids = [img_id for img_id in image_ids if img_id is not None]
|
|
|
-
|
|
|
- images = []
|
|
|
- if valid_image_ids:
|
|
|
- # 使用 IN 子句一次性查询所有图片
|
|
|
- format_strings = ','.join(['%s'] * len(valid_image_ids))
|
|
|
- query_images = f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE image_id IN ({format_strings})"
|
|
|
- cursor.execute(query_images, tuple(valid_image_ids))
|
|
|
- image_records = cursor.fetchall()
|
|
|
- images = [CardImageResponse.model_validate(row) for row in image_records]
|
|
|
+ raise HTTPException(status_code=404, detail=f"ID为 {id} 的卡牌未找到。")
|
|
|
+
|
|
|
+ # 2. 获取所有关联的图片信息
|
|
|
+ query_images = f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s"
|
|
|
+ cursor.execute(query_images, (id,))
|
|
|
+ image_records = cursor.fetchall()
|
|
|
+ images = [CardImageResponse.model_validate(row) for row in image_records]
|
|
|
|
|
|
# 组合成最终响应
|
|
|
card_response = CardDetailResponse.model_validate(card_data)
|
|
|
@@ -83,7 +72,7 @@ def get_card_details(card_id: int, db_conn: PooledMySQLConnection = db_dependenc
|
|
|
return card_response
|
|
|
|
|
|
except Exception as e:
|
|
|
- logger.error(f"查询卡牌详情失败 ({card_id}): {e}")
|
|
|
+ logger.error(f"查询卡牌详情失败 ({id}): {e}")
|
|
|
if isinstance(e, HTTPException): raise e
|
|
|
raise HTTPException(status_code=500, detail="数据库查询失败。")
|
|
|
finally:
|
|
|
@@ -100,81 +89,33 @@ def list_cards_detailed(
|
|
|
db_conn: PooledMySQLConnection = db_dependency
|
|
|
):
|
|
|
"""
|
|
|
- 获取卡牌的详细列表,支持按 card_id 范围筛选,并返回每张卡牌关联的图片ID和名称。
|
|
|
+ 获取卡牌的基础信息列表,支持按 id 范围筛选和分页。
|
|
|
"""
|
|
|
cursor = None
|
|
|
try:
|
|
|
cursor = db_conn.cursor(dictionary=True)
|
|
|
|
|
|
- # 基础查询语句,使用 LEFT JOIN 连接四次 images 表
|
|
|
- # 每次连接都用一个别名 (img_ff, img_bf, ...) 来区分
|
|
|
- base_query = f"""
|
|
|
- SELECT
|
|
|
- c.card_id,
|
|
|
- c.card_name,
|
|
|
- c.created_at,
|
|
|
- c.updated_at,
|
|
|
- img_ff.image_id AS front_face_id,
|
|
|
- img_ff.image_name AS front_face_name,
|
|
|
- img_bf.image_id AS back_face_id,
|
|
|
- img_bf.image_name AS back_face_name,
|
|
|
- img_fe.image_id AS front_edge_id,
|
|
|
- img_fe.image_name AS front_edge_name,
|
|
|
- img_be.image_id AS back_edge_id,
|
|
|
- img_be.image_name AS back_edge_name
|
|
|
- FROM
|
|
|
- {settings.DB_CARD_TABLE_NAME} AS c
|
|
|
- LEFT JOIN {settings.DB_IMAGE_TABLE_NAME} AS img_ff ON c.front_face_id = img_ff.image_id
|
|
|
- LEFT JOIN {settings.DB_IMAGE_TABLE_NAME} AS img_bf ON c.back_face_id = img_bf.image_id
|
|
|
- LEFT JOIN {settings.DB_IMAGE_TABLE_NAME} AS img_fe ON c.front_edge_id = img_fe.image_id
|
|
|
- LEFT JOIN {settings.DB_IMAGE_TABLE_NAME} AS img_be ON c.back_edge_id = img_be.image_id
|
|
|
- """
|
|
|
-
|
|
|
- # 动态构建 WHERE 条件
|
|
|
+ base_query = f"SELECT id, card_name, created_at, updated_at FROM {settings.DB_CARD_TABLE_NAME}"
|
|
|
+
|
|
|
conditions = []
|
|
|
params = []
|
|
|
if start_id is not None:
|
|
|
- conditions.append("c.card_id >= %s")
|
|
|
+ conditions.append("id >= %s")
|
|
|
params.append(start_id)
|
|
|
if end_id is not None:
|
|
|
- conditions.append("c.card_id <= %s")
|
|
|
+ conditions.append("id <= %s")
|
|
|
params.append(end_id)
|
|
|
|
|
|
if conditions:
|
|
|
base_query += " WHERE " + " AND ".join(conditions)
|
|
|
|
|
|
- # 添加排序和分页
|
|
|
- base_query += " ORDER BY c.card_id DESC LIMIT %s OFFSET %s"
|
|
|
+ base_query += " ORDER BY id DESC LIMIT %s OFFSET %s"
|
|
|
params.extend([limit, skip])
|
|
|
|
|
|
cursor.execute(base_query, tuple(params))
|
|
|
- sql_results = cursor.fetchall()
|
|
|
-
|
|
|
- # 手动将扁平的SQL结果映射到嵌套的Pydantic模型
|
|
|
- response_list = []
|
|
|
- for row in sql_results:
|
|
|
- card_data = {
|
|
|
- "card_id": row["card_id"],
|
|
|
- "card_name": row["card_name"],
|
|
|
- "created_at": row["created_at"],
|
|
|
- "updated_at": row["updated_at"],
|
|
|
- }
|
|
|
- # 检查并组装 front_face 数据
|
|
|
- if row["front_face_id"]:
|
|
|
- card_data["front_face"] = {"image_id": row["front_face_id"], "image_name": row["front_face_name"]}
|
|
|
- # 检查并组装 back_face 数据
|
|
|
- if row["back_face_id"]:
|
|
|
- card_data["back_face"] = {"image_id": row["back_face_id"], "image_name": row["back_face_name"]}
|
|
|
- # 检查并组装 front_edge 数据
|
|
|
- if row["front_edge_id"]:
|
|
|
- card_data["front_edge"] = {"image_id": row["front_edge_id"], "image_name": row["front_edge_name"]}
|
|
|
- # 检查并组装 back_edge 数据
|
|
|
- if row["back_edge_id"]:
|
|
|
- card_data["back_edge"] = {"image_id": row["back_edge_id"], "image_name": row["back_edge_name"]}
|
|
|
-
|
|
|
- response_list.append(CardListDetailResponse.model_validate(card_data))
|
|
|
-
|
|
|
- return response_list
|
|
|
+ results = cursor.fetchall()
|
|
|
+
|
|
|
+ return [CardListDetailResponse.model_validate(row) for row in results]
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"查询卡牌列表失败: {e}")
|
|
|
@@ -184,56 +125,44 @@ def list_cards_detailed(
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
|
-@router.delete("/delete/{card_id}", status_code=200, summary="删除卡牌及其所有关联图片")
|
|
|
-def delete_card(card_id: int, db_conn: PooledMySQLConnection = db_dependency):
|
|
|
+@router.delete("/delete/{id}", status_code=200, summary="删除卡牌及其所有关联图片")
|
|
|
+def delete_card(id: int, db_conn: PooledMySQLConnection = db_dependency):
|
|
|
"""
|
|
|
- 删除一张卡牌。由于外键约束ON DELETE SET NULL,仅删除卡牌记录。
|
|
|
- 如果需要同时删除图片,需要先查询图片并手动删除。
|
|
|
- 我们将实现级联删除图片文件。
|
|
|
+ 删除一张卡牌及其所有关联的图片记录和物理文件。
|
|
|
+ 利用了数据库的 ON DELETE CASCADE 特性。
|
|
|
"""
|
|
|
cursor = None
|
|
|
try:
|
|
|
cursor = db_conn.cursor()
|
|
|
|
|
|
- # 1. 查询所有关联图片的路径
|
|
|
- query_paths = f"""
|
|
|
- SELECT i.image_path
|
|
|
- FROM {settings.DB_CARD_TABLE_NAME} c
|
|
|
- JOIN {settings.DB_IMAGE_TABLE_NAME} i ON i.image_id IN
|
|
|
- (c.front_face_id, c.back_face_id, c.front_edge_id, c.back_edge_id)
|
|
|
- WHERE c.card_id = %s
|
|
|
- """
|
|
|
- cursor.execute(query_paths, (card_id,))
|
|
|
- image_paths = [row[0] for row in cursor.fetchall()]
|
|
|
-
|
|
|
- # 2. 删除卡牌记录 (这将触发删除图片记录,因为我们下面的逻辑会删除图片)
|
|
|
- # 注意: 如果直接删除 card, 外键设为 SET NULL, 图片记录不会被删。所以我们先删图片,再删card
|
|
|
-
|
|
|
- # 3. 删除图片记录
|
|
|
- if image_paths:
|
|
|
- format_strings = ','.join(['%s'] * len(image_paths))
|
|
|
- query_delete_images = f"DELETE FROM {settings.DB_IMAGE_TABLE_NAME} WHERE image_path IN ({format_strings})"
|
|
|
- cursor.execute(query_delete_images, tuple(image_paths))
|
|
|
-
|
|
|
- # 4. 删除卡牌记录
|
|
|
- query_delete_card = f"DELETE FROM {settings.DB_CARD_TABLE_NAME} WHERE card_id = %s"
|
|
|
- cursor.execute(query_delete_card, (card_id,))
|
|
|
+ # 1. 查询所有关联图片的物理文件路径,以便稍后删除
|
|
|
+ query_paths = f"SELECT image_path FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s"
|
|
|
+ cursor.execute(query_paths, (id,))
|
|
|
+ image_paths_to_delete = [row[0] for row in cursor.fetchall()]
|
|
|
+
|
|
|
+ # 2. 删除卡牌记录。数据库会自动级联删除 card_images 表中的相关记录
|
|
|
+ query_delete_card = f"DELETE FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s"
|
|
|
+ cursor.execute(query_delete_card, (id,))
|
|
|
|
|
|
if cursor.rowcount == 0:
|
|
|
- raise HTTPException(status_code=404, detail=f"ID为 {card_id} 的卡牌未找到。")
|
|
|
+ raise HTTPException(status_code=404, detail=f"ID为 {id} 的卡牌未找到。")
|
|
|
|
|
|
- # 5. 删除物理文件
|
|
|
- for path in image_paths:
|
|
|
+ # 3. 删除物理文件
|
|
|
+ for path in image_paths_to_delete:
|
|
|
if os.path.exists(path):
|
|
|
- os.remove(path)
|
|
|
- logger.info(f"图片文件已删除: {path}")
|
|
|
+ try:
|
|
|
+ os.remove(path)
|
|
|
+ logger.info(f"图片文件已删除: {path}")
|
|
|
+ except OSError as e:
|
|
|
+ logger.error(f"删除文件失败 {path}: {e}")
|
|
|
|
|
|
db_conn.commit()
|
|
|
- logger.info(f"ID {card_id} 的卡牌和关联文件已成功删除。")
|
|
|
- return {"message": f"成功删除卡牌 ID {card_id} 及其所有关联数据"}
|
|
|
+ logger.info(f"ID {id} 的卡牌和关联数据已成功删除。")
|
|
|
+ return {"message": f"成功删除卡牌 ID {id} 及其所有关联数据"}
|
|
|
+
|
|
|
except Exception as e:
|
|
|
db_conn.rollback()
|
|
|
- logger.error(f"删除卡牌失败 ({card_id}): {e}")
|
|
|
+ logger.error(f"删除卡牌失败 ({id}): {e}")
|
|
|
if isinstance(e, HTTPException): raise e
|
|
|
raise HTTPException(status_code=500, detail="删除卡牌失败。")
|
|
|
finally:
|