瀏覽代碼

为cards表增加几个字段, 各种crud操作会影响cards的对应id的字段, 包括分数等

AnlaAnla 4 周之前
父節點
當前提交
338081f560

+ 1 - 0
.idea/encodings.xml

@@ -6,6 +6,7 @@
     <file url="file://$PROJECT_DIR$/Test/test01.py" charset="GBK" />
     <file url="file://$PROJECT_DIR$/Test/test01.py" charset="GBK" />
     <file url="file://$PROJECT_DIR$/app/api/cards.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/app/api/cards.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/app/api/images.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/app/api/images.py" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/app/crud/crud_card.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/app/utils/card_score_calculate.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/app/utils/card_score_calculate.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/app/utils/scheme.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/app/utils/scheme.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/run.py" charset="UTF-8" />
     <file url="file://$PROJECT_DIR$/run.py" charset="UTF-8" />

+ 5 - 2
Test/img_score_and_insert.py

@@ -8,8 +8,11 @@ from datetime import datetime
 
 
 # --- 配置区域 ---
 # --- 配置区域 ---
 # 1. 服务 URL
 # 1. 服务 URL
-INFERENCE_SERVICE_URL = "http://192.168.31.243:7744"
-STORAGE_SERVICE_URL = "http://192.168.31.243:7745"
+INFERENCE_SERVICE_URL = "http://127.0.0.1:7744"
+STORAGE_SERVICE_URL = "http://127.0.0.1:7745"
+
+# INFERENCE_SERVICE_URL = "http://192.168.31.243:7744"
+# STORAGE_SERVICE_URL = "http://192.168.31.243:7745"
 
 
 # 2. 要处理的卡片信息
 # 2. 要处理的卡片信息
 formate_time = datetime.now().strftime("%Y-%m-%d_%H:%M")
 formate_time = datetime.now().strftime("%Y-%m-%d_%H:%M")

+ 2 - 2
Test/test01.py

@@ -33,8 +33,8 @@ def send(url):
 
 
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
-    # base_url = 'http://127.0.0.1:7745/api/cards/query/9'
-    base_url = 'http://127.0.0.1:7745/api/cards/query_next/5'
+    base_url = 'http://127.0.0.1:7745/api/cards/query/1'
+    # base_url = 'http://127.0.0.1:7745/api/cards/query_next/5'
 
 
     # base_url = 'http://192.168.31.243:7745/api/cards/query/6'
     # base_url = 'http://192.168.31.243:7745/api/cards/query/6'
     send(base_url)
     send(base_url)

+ 77 - 145
app/api/cards.py

@@ -2,13 +2,16 @@ from datetime import datetime
 import os
 import os
 from typing import Optional, List
 from typing import Optional, List
 from fastapi import APIRouter, Depends, HTTPException, Query
 from fastapi import APIRouter, Depends, HTTPException, Query
+
 from mysql.connector.pooling import PooledMySQLConnection
 from mysql.connector.pooling import PooledMySQLConnection
 
 
 from app.core.config import settings
 from app.core.config import settings
 from app.core.logger import get_logger
 from app.core.logger import get_logger
 from app.core.database_loader import get_db_connection
 from app.core.database_loader import get_db_connection
-from app.utils.card_score_calculate import card_score_calculate
-from app.utils.scheme import CardDetailResponse, CardImageResponse, CardListDetailResponse
+from app.utils.scheme import (
+    CardDetailResponse, CardListDetailResponse, CardType, SortBy, SortOrder
+)
+from app.crud import crud_card
 
 
 logger = get_logger(__name__)
 logger = get_logger(__name__)
 router = APIRouter()
 router = APIRouter()
@@ -18,152 +21,90 @@ db_dependency = Depends(get_db_connection)
 @router.post("/created", response_model=CardDetailResponse, status_code=201, summary="创建一个新的卡牌记录")
 @router.post("/created", response_model=CardDetailResponse, status_code=201, summary="创建一个新的卡牌记录")
 def create_card(
 def create_card(
         card_name: Optional[str] = Query(None, summary="卡牌的名称"),
         card_name: Optional[str] = Query(None, summary="卡牌的名称"),
+        card_type: CardType = Query(CardType.pokemon, summary="卡牌类型"),
         db_conn: PooledMySQLConnection = db_dependency
         db_conn: PooledMySQLConnection = db_dependency
 ):
 ):
     """创建一个新的卡牌实体,此时它不关联任何图片。"""
     """创建一个新的卡牌实体,此时它不关联任何图片。"""
-    cursor = None
     try:
     try:
-        cursor = db_conn.cursor()
-        query = f"INSERT INTO {settings.DB_CARD_TABLE_NAME} (card_name) VALUES (%s)"
-        cursor.execute(query, (card_name,))
-        db_conn.commit()
-        new_id = cursor.lastrowid
-        logger.info(f"新卡牌已创建, ID: {new_id}")
-
-        # 返回刚创建的空卡牌信息
-        return CardDetailResponse(
-            id=new_id,
-            card_name=card_name,
-            created_at=datetime.now(),
-            updated_at=datetime.now(),
-            images=[]
-        )
+        with db_conn.cursor(dictionary=True) as cursor:
+
+            query = f"INSERT INTO {settings.DB_CARD_TABLE_NAME} (card_name, card_type) VALUES (%s, %s)"
+            cursor.execute(query, (card_name, card_type.value))
+            db_conn.commit()
+            new_id = cursor.lastrowid
+            logger.info(f"新卡牌已创建, ID: {new_id}, 类型: {card_type.value}")
+
+            # 返回刚创建的空卡牌信息
+            cursor.execute(f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s", (new_id,))
+            new_card_data = cursor.fetchone()
+
+            response_data = {**new_card_data, "images": []}
+            return CardDetailResponse.model_validate(response_data)
+
     except Exception as e:
     except Exception as e:
         db_conn.rollback()
         db_conn.rollback()
         logger.error(f"创建卡牌失败: {e}")
         logger.error(f"创建卡牌失败: {e}")
         raise HTTPException(status_code=500, detail="数据库插入失败。")
         raise HTTPException(status_code=500, detail="数据库插入失败。")
-    finally:
-        if cursor:
-            cursor.close()
 
 
 
 
 @router.get("/query/{id}", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息")
 @router.get("/query/{id}", response_model=CardDetailResponse, summary="获取指定卡牌的详细信息")
 def get_card_details(id: int, db_conn: PooledMySQLConnection = db_dependency):
 def get_card_details(id: int, db_conn: PooledMySQLConnection = db_dependency):
-    """获取卡牌元数据以及所有与之关联的图片信息。"""
-    cursor = None
-    try:
-        cursor = db_conn.cursor(dictionary=True)
-
-        # 1. 获取卡牌信息
-        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为 {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]
-
-        # 计算总分数(只有当图片数量为 4 时才计算)
-        card_response = card_score_calculate(card_data, images)
-
-        return card_response
-
-    except Exception as e:
-        logger.error(f"查询卡牌详情失败 ({id}): {e}")
-        if isinstance(e, HTTPException): raise e
-        raise HTTPException(status_code=500, detail="数据库查询失败。")
-    finally:
-        if cursor:
-            cursor.close()
+    """获取卡牌元数据以及所有与之关联的图片信息。分数是预先计算好的。"""
+    # REFACTORED: Use CRUD function
+    card_data = crud_card.get_card_with_details(db_conn, id)
+    if not card_data:
+        raise HTTPException(status_code=404, detail=f"ID为 {id} 的卡牌未找到。")
+    return CardDetailResponse.model_validate(card_data)
 
 
 
 
 @router.get("/query_next/{id}", response_model=CardDetailResponse, summary="获取指定卡牌id的下一个卡的详细信息")
 @router.get("/query_next/{id}", response_model=CardDetailResponse, summary="获取指定卡牌id的下一个卡的详细信息")
-def get_card_details(id: int, db_conn: PooledMySQLConnection = db_dependency):
+def get_next_card_details(id: int, db_conn: PooledMySQLConnection = db_dependency):  # Renamed function
     """获取指定ID的下一张卡牌的元数据以及所有与之关联的图片信息。"""
     """获取指定ID的下一张卡牌的元数据以及所有与之关联的图片信息。"""
     try:
     try:
         with db_conn.cursor(dictionary=True) as cursor:
         with db_conn.cursor(dictionary=True) as cursor:
-
-            # 1. 获取下一张卡牌的
             query_next_card = (
             query_next_card = (
-                f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} "
+                f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} "
                 f"WHERE id > %s ORDER BY id ASC LIMIT 1"
                 f"WHERE id > %s ORDER BY id ASC LIMIT 1"
             )
             )
             cursor.execute(query_next_card, (id,))
             cursor.execute(query_next_card, (id,))
-            next_card_data = cursor.fetchone()
+            next_card_row = cursor.fetchone()
 
 
-            # 如果没有找到下一张卡牌,则抛出 404 错误
-            if not next_card_data:
+            if not next_card_row:
                 raise HTTPException(status_code=404, detail=f"ID为 {id} 的下一个卡牌未找到。")
                 raise HTTPException(status_code=404, detail=f"ID为 {id} 的下一个卡牌未找到。")
 
 
-            # 从获取到的下一张卡牌数据中提取其ID
-            next_card_id = next_card_data['id']
-
-            # 2. 使用【下一个卡牌的ID】来获取所有关联的图片信息
-            query_images = f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s"
-            cursor.execute(query_images, (next_card_id,))  # <-- 关键修正:使用 next_card_id
-            image_records = cursor.fetchall()
-            images = [CardImageResponse.model_validate(row) for row in image_records]
-
-            # 3. 使用【正确的下一张卡牌数据】和【正确的图片数据】进行计算
-            card_response = card_score_calculate(next_card_data, images)  # <-- 关键修正:传入 next_card_data
+            next_card_id = next_card_row['id']
+            # 获取单个卡牌的完整信息
+            card_data = crud_card.get_card_with_details(db_conn, next_card_id)
+            if not card_data:
+                raise HTTPException(status_code=404, detail=f"下一个卡牌ID {next_card_id} 未找到详细信息。")
 
 
-            return card_response
+            return CardDetailResponse.model_validate(card_data)
 
 
     except Exception as e:
     except Exception as e:
         logger.error(f"查询下一个卡牌详情失败 (基准ID: {id}): {e}")
         logger.error(f"查询下一个卡牌详情失败 (基准ID: {id}): {e}")
-        # 如果异常已经是 HTTPException,直接重新抛出,否则包装成 500 错误
-        if isinstance(e, HTTPException):
-            raise e
+        if isinstance(e, HTTPException): raise e
         raise HTTPException(status_code=500, detail="服务器内部错误,查询数据库失败。")
         raise HTTPException(status_code=500, detail="服务器内部错误,查询数据库失败。")
 
 
 
 
-@router.get("/card_list", response_model=List[CardListDetailResponse], summary="获取卡牌列表")
-def list_cards_detailed(
-        start_id: Optional[int] = Query(None, description="筛选条件:起始 card_id"),
-        end_id: Optional[int] = Query(None, description="筛选条件:结束 card_id"),
+@router.get("/card_list", response_model=List[CardListDetailResponse], summary="获取卡牌列表(支持筛选和排序)")
+def list_cards_detailed(  # MODIFIED
+        card_name: Optional[str] = Query(None, description="筛选条件:卡牌名称 (模糊匹配)"),
+        card_type: Optional[CardType] = Query(None, description="筛选条件:卡牌类型"),
+        sort_by: SortBy = Query(SortBy.updated_at, description="排序字段"),
+        sort_order: SortOrder = Query(SortOrder.desc, description="排序顺序"),
         skip: int = Query(0, ge=0, description="分页:跳过的记录数"),
         skip: int = Query(0, ge=0, description="分页:跳过的记录数"),
         limit: int = Query(100, ge=1, le=1000, description="分页:每页的记录数"),
         limit: int = Query(100, ge=1, le=1000, description="分页:每页的记录数"),
         db_conn: PooledMySQLConnection = db_dependency
         db_conn: PooledMySQLConnection = db_dependency
 ):
 ):
-    """
-    获取卡牌的基础信息列表,支持按 id 范围筛选和分页。
-    """
-    cursor = None
+    """获取卡牌的基础信息列表,支持按名称、类型筛选,以及多字段排序和分页。"""
     try:
     try:
-        cursor = db_conn.cursor(dictionary=True)
-
-        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("id >= %s")
-            params.append(start_id)
-        if end_id is not None:
-            conditions.append("id <= %s")
-            params.append(end_id)
-
-        if conditions:
-            base_query += " WHERE " + " AND ".join(conditions)
-
-        base_query += " ORDER BY id DESC LIMIT %s OFFSET %s"
-        params.extend([limit, skip])
-
-        cursor.execute(base_query, tuple(params))
-        results = cursor.fetchall()
-
-        return [CardListDetailResponse.model_validate(row) for row in results]
-
+        cards_with_images = crud_card.get_card_list_with_images(
+            db_conn, card_name, card_type, sort_by, sort_order, skip, limit
+        )
+        return [CardListDetailResponse.model_validate(c) for c in cards_with_images]
     except Exception as e:
     except Exception as e:
         logger.error(f"查询卡牌列表失败: {e}")
         logger.error(f"查询卡牌列表失败: {e}")
         raise HTTPException(status_code=500, detail="获取数据列表失败。")
         raise HTTPException(status_code=500, detail="获取数据列表失败。")
-    finally:
-        if cursor:
-            cursor.close()
 
 
 
 
 @router.delete("/delete/{id}", status_code=200, summary="删除卡牌及其所有关联图片")
 @router.delete("/delete/{id}", status_code=200, summary="删除卡牌及其所有关联图片")
@@ -172,47 +113,38 @@ def delete_card(id: int, db_conn: PooledMySQLConnection = db_dependency):
     删除一张卡牌及其所有关联的图片记录和物理文件。
     删除一张卡牌及其所有关联的图片记录和物理文件。
     利用了数据库的 ON DELETE CASCADE 特性。
     利用了数据库的 ON DELETE CASCADE 特性。
     """
     """
-    cursor = None
     try:
     try:
-        cursor = db_conn.cursor()
-
-        # 1. 查询所有关联图片的所有物理文件路径,以便稍后删除 (修改部分)
-        query_paths = (f"SELECT image_path, detection_image_path, modified_image_path "
-                       f"FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s")
-        cursor.execute(query_paths, (id,))
-
-        image_paths_to_delete = []
-        for row in cursor.fetchall():
-            # 将每一行中非空的路径都添加到待删除列表
-            image_paths_to_delete.extend([path for path in row if path])
-
-        # 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为 {id} 的卡牌未找到。")
-
-        # 3. 删除物理文件
-        for path in image_paths_to_delete:
-            absolute_path = settings.BASE_PATH / path.lstrip('/\\')
-
-            if os.path.exists(absolute_path):
-                try:
-                    os.remove(absolute_path)
-                    logger.info(f"图片文件已删除: {absolute_path}")
-                except OSError as e:
-                    logger.error(f"删除文件失败 {absolute_path}: {e}")
-
-        db_conn.commit()
-        logger.info(f"ID {id} 的卡牌和关联数据已成功删除。")
-        return {"message": f"成功删除卡牌 ID {id} 及其所有关联数据"}
+        with db_conn.cursor() as cursor:
+            # 1. 查询所有关联图片的所有物理文件路径
+            query_paths = (f"SELECT image_path, detection_image_path, modified_image_path "
+                           f"FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s")
+            cursor.execute(query_paths, (id,))
+            image_paths_to_delete = [path for row in cursor.fetchall() for path in row if path]
+
+            # 2. 删除卡牌记录
+            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为 {id} 的卡牌未找到。")
+
+            db_conn.commit()
+            logger.info(f"ID {id} 的卡牌和关联数据已成功删除。")
+
+            # 3. 删除物理文件
+            for path in image_paths_to_delete:
+                absolute_path = settings.BASE_PATH / path.lstrip('/\\')
+                if os.path.exists(absolute_path):
+                    try:
+                        os.remove(absolute_path)
+                        logger.info(f"图片文件已删除: {absolute_path}")
+                    except OSError as e:
+                        logger.error(f"删除文件失败 {absolute_path}: {e}")
+
+            return {"message": f"成功删除卡牌 ID {id} 及其所有关联数据"}
 
 
     except Exception as e:
     except Exception as e:
         db_conn.rollback()
         db_conn.rollback()
         logger.error(f"删除卡牌失败 ({id}): {e}")
         logger.error(f"删除卡牌失败 ({id}): {e}")
         if isinstance(e, HTTPException): raise e
         if isinstance(e, HTTPException): raise e
         raise HTTPException(status_code=500, detail="删除卡牌失败。")
         raise HTTPException(status_code=500, detail="删除卡牌失败。")
-    finally:
-        if cursor:
-            cursor.close()

+ 21 - 9
app/api/images.py

@@ -14,6 +14,7 @@ from app.core.logger import get_logger
 from app.utils.scheme import CardImageResponse, ImageJsonPairResponse, ResultImagePathType
 from app.utils.scheme import CardImageResponse, ImageJsonPairResponse, ResultImagePathType
 from app.utils.scheme import ImageType, IMAGE_TYPE_TO_SCORE_TYPE
 from app.utils.scheme import ImageType, IMAGE_TYPE_TO_SCORE_TYPE
 from app.core.database_loader import get_db_connection
 from app.core.database_loader import get_db_connection
+from app.crud import crud_card
 
 
 logger = get_logger(__name__)
 logger = get_logger(__name__)
 router = APIRouter()
 router = APIRouter()
@@ -73,6 +74,13 @@ async def upload_image_for_card(
         db_conn.commit()
         db_conn.commit()
         logger.info(f"图片 {new_id} 已成功关联到卡牌 {card_id},类型为 {image_type.value}。")
         logger.info(f"图片 {new_id} 已成功关联到卡牌 {card_id},类型为 {image_type.value}。")
 
 
+        try:
+            crud_card.update_card_scores_and_status(db_conn, card_id)
+            logger.info(f"卡牌 {card_id} 的分数和状态已更新。")
+        except Exception as score_update_e:
+            # 即使分数更新失败,图片也已经成功插入,记录错误但不回滚
+            logger.error(f"更新卡牌 {card_id} 分数失败: {score_update_e}")
+
         cursor.execute(f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,))
         cursor.execute(f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (new_id,))
         new_image_data = cursor.fetchone()
         new_image_data = cursor.fetchone()
         columns = [desc[0] for desc in cursor.description]
         columns = [desc[0] for desc in cursor.description]
@@ -133,6 +141,7 @@ async def update_image_modified_json(
     根据 id 获取 image_type, 调用外部接口重新计算分数, 并更新 modified_json。
     根据 id 获取 image_type, 调用外部接口重新计算分数, 并更新 modified_json。
     updated_at 会自动更新
     updated_at 会自动更新
     """
     """
+    card_id_to_update = None
     cursor = None
     cursor = None
     try:
     try:
         cursor = db_conn.cursor(dictionary=True)
         cursor = db_conn.cursor(dictionary=True)
@@ -143,6 +152,7 @@ async def update_image_modified_json(
         if not row:
         if not row:
             raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
             raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
 
 
+        card_id_to_update = row["card_id"]
         image_type = row["image_type"]
         image_type = row["image_type"]
         score_type = IMAGE_TYPE_TO_SCORE_TYPE.get(image_type)
         score_type = IMAGE_TYPE_TO_SCORE_TYPE.get(image_type)
         if not score_type:
         if not score_type:
@@ -150,14 +160,10 @@ async def update_image_modified_json(
 
 
         # 2️ 调用远程计算接口
         # 2️ 调用远程计算接口
         try:
         try:
-            params = {"score_type": score_type}
-            payload = new_json_data
             response = await run_in_threadpool(
             response = await run_in_threadpool(
                 lambda: requests.post(
                 lambda: requests.post(
                     settings.SCORE_RECALCULATE_ENDPOINT,
                     settings.SCORE_RECALCULATE_ENDPOINT,
-                    params=params,
-                    json=payload,
-                    timeout=20
+                    params={"score_type": score_type}, json=new_json_data, timeout=20
                 )
                 )
             )
             )
         except Exception as e:
         except Exception as e:
@@ -168,21 +174,27 @@ async def update_image_modified_json(
             raise HTTPException(status_code=response.status_code,
             raise HTTPException(status_code=response.status_code,
                                 detail=f"分数计算接口返回错误: {response.text}")
                                 detail=f"分数计算接口返回错误: {response.text}")
 
 
-        recalculated_json = response.json()
-
         # 3️ 保存结果到数据库
         # 3️ 保存结果到数据库
-        recalculated_json_str = json.dumps(recalculated_json, ensure_ascii=False)
+        recalculated_json_str = json.dumps(response.json(), ensure_ascii=False)
         update_query = f"UPDATE {settings.DB_IMAGE_TABLE_NAME} SET modified_json = %s WHERE id = %s"
         update_query = f"UPDATE {settings.DB_IMAGE_TABLE_NAME} SET modified_json = %s WHERE id = %s"
+
         cursor.execute(update_query, (recalculated_json_str, id))
         cursor.execute(update_query, (recalculated_json_str, id))
 
 
         logger.info(f"更新 id={id} 的操作影响了 {cursor.rowcount} 行。")
         logger.info(f"更新 id={id} 的操作影响了 {cursor.rowcount} 行。")
 
 
         if cursor.rowcount == 0:
         if cursor.rowcount == 0:
-            raise HTTPException(status_code=500, detail=f"更新失败,ID为 {id} 的记录未找到或数据未发生变化。")
+            logger.warning(f"更新 id={id} 的 modified_json 未影响任何行。")
 
 
         db_conn.commit()
         db_conn.commit()
         logger.info(f"图片ID {id} 的 modified_json 已更新并重新计算。")
         logger.info(f"图片ID {id} 的 modified_json 已更新并重新计算。")
 
 
+        # 更新对应的 cards 的分数状态
+        try:
+            crud_card.update_card_scores_and_status(db_conn, card_id_to_update)
+            logger.info(f"卡牌 {card_id_to_update} 的分数和状态已更新。")
+        except Exception as score_update_e:
+            logger.error(f"更新卡牌 {card_id_to_update} 分数失败: {score_update_e}")
+
         return {
         return {
             "message": f"成功更新图片ID {id} 的JSON数据",
             "message": f"成功更新图片ID {id} 的JSON数据",
             "image_type": image_type,
             "image_type": image_type,

+ 3 - 4
app/core/config.py

@@ -5,12 +5,11 @@ import json
 
 
 
 
 class Settings:
 class Settings:
-
     BASE_PATH = Path(__file__).parent.parent.parent.absolute()
     BASE_PATH = Path(__file__).parent.parent.parent.absolute()
 
 
     CONFIG_PATH = BASE_PATH / 'Config.json'
     CONFIG_PATH = BASE_PATH / 'Config.json'
 
 
-    API_PREFIX: str = "/api" # 通用前缀
+    API_PREFIX: str = "/api"  # 通用前缀
 
 
     DATA_DIR = BASE_PATH / "Data"
     DATA_DIR = BASE_PATH / "Data"
     SCORE_CONFIG_PATH = BASE_PATH / "app/core/scoring_config.json"
     SCORE_CONFIG_PATH = BASE_PATH / "app/core/scoring_config.json"
@@ -24,7 +23,7 @@ class Settings:
     # 从 Config.json 读取的旧表名, 我们将不再使用它, 但保留以兼容旧文件
     # 从 Config.json 读取的旧表名, 我们将不再使用它, 但保留以兼容旧文件
     # 建议直接在代码中定义新表名, 避免混淆
     # 建议直接在代码中定义新表名, 避免混淆
     DB_CARD_TABLE_NAME = 'cards'
     DB_CARD_TABLE_NAME = 'cards'
-    DB_IMAGE_TABLE_NAME = 'card_images' # 新的图片表名
+    DB_IMAGE_TABLE_NAME = 'card_images'  # 新的图片表名
 
 
     DATABASE_CONFIG: Dict[str, str] = {
     DATABASE_CONFIG: Dict[str, str] = {
         'user': 'root',
         'user': 'root',
@@ -47,4 +46,4 @@ class Settings:
 
 
 settings = Settings()
 settings = Settings()
 print(f"项目根目录: {settings.BASE_PATH}")
 print(f"项目根目录: {settings.BASE_PATH}")
-print(f"数据存储目录: {settings.DATA_DIR}")
+print(f"数据存储目录: {settings.DATA_DIR}")

+ 4 - 0
app/core/database_loader.py

@@ -29,6 +29,10 @@ def init_database():
             f"CREATE TABLE IF NOT EXISTS `{settings.DB_CARD_TABLE_NAME}` ("
             f"CREATE TABLE IF NOT EXISTS `{settings.DB_CARD_TABLE_NAME}` ("
             "  `id` INT AUTO_INCREMENT PRIMARY KEY,"
             "  `id` INT AUTO_INCREMENT PRIMARY KEY,"
             "  `card_name` VARCHAR(255) NULL COMMENT '卡牌的通用名称',"
             "  `card_name` VARCHAR(255) NULL COMMENT '卡牌的通用名称',"
+            "  `card_type` VARCHAR(50) NOT NULL DEFAULT 'pokemon' COMMENT '卡牌类型 (pokemon, basketball, etc)',"
+            "  `detection_score` DECIMAL(4, 2) NULL COMMENT '原始检测总分',"
+            "  `modified_score` DECIMAL(4, 2) NULL COMMENT '修改后总分',"
+            "  `is_edited` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否被编辑过',"
             "  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
             "  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"
             "  `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
             "  `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
             ") ENGINE=InnoDB COMMENT='存储实体卡牌的核心信息'"
             ") ENGINE=InnoDB COMMENT='存储实体卡牌的核心信息'"

+ 0 - 0
app/crud/__init__.py


+ 137 - 0
app/crud/crud_card.py

@@ -0,0 +1,137 @@
+from typing import Optional, List, Dict, Any
+from mysql.connector.pooling import PooledMySQLConnection
+import json
+from datetime import datetime
+
+from app.core.config import settings
+from app.utils.scheme import CardImageResponse, CardType, SortBy, SortOrder
+from app.utils.card_score_calculate import calculate_scores_from_images
+
+
+def update_card_scores_and_status(db_conn: PooledMySQLConnection, card_id: int):
+    """
+    根据卡牌关联的图片数量和内容,更新cards表中的分数和状态。
+    - 如果图片满4张,计算并更新 detection_score, modified_score, is_edited。
+    - 如果图片不足4张,将分数置为NULL,is_edited置为False。
+    - 自动更新 updated_at 字段。
+    """
+    with db_conn.cursor(dictionary=True) as cursor:
+        # 1. 获取所有关联图片
+        query_images = f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s"
+        cursor.execute(query_images, (card_id,))
+        image_records = cursor.fetchall()
+
+        # 将数据库行转换为Pydantic模型,便于处理JSON
+        images = [CardImageResponse.model_validate(row) for row in image_records]
+
+        # 2. 计算分数和状态
+        scores_data = calculate_scores_from_images(images)
+
+        # 3. 更新 cards 表
+        # 注意: updated_at 会由数据库自动更新
+        query_update_card = (
+            f"UPDATE {settings.DB_CARD_TABLE_NAME} SET "
+            "detection_score = %s, modified_score = %s, is_edited = %s, updated_at = %s "
+            "WHERE id = %s"
+        )
+        params = (
+            scores_data["detection_score"],
+            scores_data["modified_score"],
+            scores_data["is_edited"],
+            datetime.now(),  # 手动更新时间戳以确保触发
+            card_id,
+        )
+        cursor.execute(query_update_card, params)
+        db_conn.commit()
+
+
+def get_card_with_details(db_conn: PooledMySQLConnection, card_id: int) -> Optional[Dict[str, Any]]:
+    """获取单个卡牌的完整信息,包括预计算的分数和所有图片详情。"""
+    with db_conn.cursor(dictionary=True) as cursor:
+        # 1. 获取卡牌信息
+        query_card = f"SELECT * FROM {settings.DB_CARD_TABLE_NAME} WHERE id = %s"
+        cursor.execute(query_card, (card_id,))
+        card_data = cursor.fetchone()
+        if not card_data:
+            return None
+
+        # 2. 获取所有关联的图片信息
+        query_images = f"SELECT * FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id = %s"
+        cursor.execute(query_images, (card_id,))
+        image_records = cursor.fetchall()
+        images = [CardImageResponse.model_validate(row) for row in image_records]
+
+        # 3. 获取分数详情 (如果需要)
+        # 只有当图片满4张时,分数详情才有意义
+        score_details = calculate_scores_from_images(images)
+        card_data.update({
+            "images": images,
+            "detection_score_detail": score_details["detection_score_detail"],
+            "modified_score_detail": score_details["modified_score_detail"]
+        })
+
+        return card_data
+
+
+def get_card_list_with_images(
+        db_conn: PooledMySQLConnection,
+        card_name: Optional[str],
+        card_type: Optional[CardType],
+        sort_by: SortBy,
+        sort_order: SortOrder,
+        skip: int,
+        limit: int
+) -> List[Dict[str, Any]]:
+    """获取带筛选和排序功能的卡牌列表,并附带其关联的图片信息。"""
+    with db_conn.cursor(dictionary=True) as cursor:
+        # 1. 构建动态查询
+        query = f"SELECT * FROM {settings.DB_CARD_TABLE_NAME}"
+        conditions = []
+        params = []
+
+        if card_name:
+            conditions.append("card_name LIKE %s")
+            params.append(f"%{card_name}%")
+        if card_type:
+            conditions.append("card_type = %s")
+            params.append(card_type.value)
+
+        if conditions:
+            query += " WHERE " + " AND ".join(conditions)
+
+        # 添加排序和分页
+        query += f" ORDER BY {sort_by.value} {sort_order.value}, id DESC"
+        query += " LIMIT %s OFFSET %s"
+        params.extend([limit, skip])
+
+        cursor.execute(query, tuple(params))
+        cards = cursor.fetchall()
+
+        if not cards:
+            return []
+
+        # 2. 一次性获取所有相关卡牌的图片 (避免 N+1 查询)
+        card_ids = [card['id'] for card in cards]
+
+        # 使用 IN 子句和占位符
+        format_strings = ','.join(['%s'] * len(card_ids))
+        image_query = (
+            f"SELECT id, card_id, image_type, image_path, detection_image_path, modified_image_path "
+            f"FROM {settings.DB_IMAGE_TABLE_NAME} WHERE card_id IN ({format_strings})"
+        )
+        cursor.execute(image_query, tuple(card_ids))
+        images = cursor.fetchall()
+
+        # 3. 将图片按 card_id 分组
+        images_by_card_id = {}
+        for image in images:
+            cid = image['card_id']
+            if cid not in images_by_card_id:
+                images_by_card_id[cid] = []
+            images_by_card_id[cid].append(image)
+
+        # 4. 将图片附加到对应的卡牌上
+        for card in cards:
+            card['images'] = images_by_card_id.get(card['id'], [])
+
+        return cards

+ 0 - 0
app/utils/__init__.py


+ 125 - 125
app/utils/card_score_calculate.py

@@ -1,144 +1,144 @@
-from app.utils.scheme import CardDetailResponse, ImageType
+from app.utils.scheme import ImageType, CardImageResponse
 from app.core.logger import get_logger
 from app.core.logger import get_logger
-from typing import List
+from typing import List, Dict, Any
 
 
 logger = get_logger(__name__)
 logger = get_logger(__name__)
 
 
 
 
-def card_score_calculate(card_data: dict, images: List) -> CardDetailResponse:
-    card_data["detection_score"] = None
-    card_data["modified_score"] = None
-    card_data["detection_score_detail"] = {}
-    card_data["modified_score_detail"] = {}
-    if len(images) == 4:
-        try:
-            # ---------- detection_score ----------
-            detection_score = 10.0
-            detection_center_score = 10.0
-            detection_corner_score = 10.0
-            detection_edge_score = 10.0
-            detection_face_score = 10.0
+def calculate_scores_from_images(images: List[CardImageResponse]) -> Dict[str, Any]:
+    """
+    根据一个包含4张图片的列表,计算原始分数和修改后分数。
+    返回一个包含分数详情的字典。
+    """
+    scores = {
+        "detection_score": None,
+        "modified_score": None,
+        "detection_score_detail": {
+            "detection_center_score": None,
+            "detection_corner_score": None,
+            "detection_edge_score": None,
+            "detection_face_score": None
+        },
+        "modified_score_detail": {
+            "modified_center_score": None,
+            "modified_corner_score": None,
+            "modified_edge_score": None,
+            "modified_face_score": None
+        },
+        "is_edited": False,
+    }
+
+    if len(images) != 4:
+        return scores
+
+    try:
+        # ---------- detection_score ----------
+        detection_score = 10.0
+        detection_center_score = 10.0
+        detection_corner_score = 10.0
+        detection_edge_score = 10.0
+        detection_face_score = 10.0
+        for img in images:
+            try:
+                add_val = img.detection_json.get("result", {}).get("_used_compute_deduct_score", 0)
+                detection_score += float(add_val or 0)
+
+                if img.image_type == ImageType.front_edge:
+                    center_reduct_val = img.detection_json.get("result", {}).get("center_result", {}).get(
+                        "deduct_score", 0)
+                    corner_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
+                        "front_corner_deduct_score", 0)
+                    edge_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
+                        "front_edge_deduct_score", 0)
+                    detection_center_score += float(center_reduct_val or 0)
+                    detection_corner_score += float(corner_reduct_val or 0)
+                    detection_edge_score += float(edge_reduct_val or 0)
+                elif img.image_type == ImageType.back_edge:
+                    center_reduct_val = img.detection_json.get("result", {}).get("center_result", {}).get(
+                        "deduct_score", 0)
+                    corner_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
+                        "back_corner_deduct_score", 0)
+                    edge_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
+                        "back_edge_deduct_score", 0)
+                    detection_center_score += float(center_reduct_val or 0)
+                    detection_corner_score += float(corner_reduct_val or 0)
+                    detection_edge_score += float(edge_reduct_val or 0)
+                elif img.image_type == ImageType.front_face:
+                    face_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
+                        "front_face_deduct_score", 0)
+                    detection_face_score += float(face_reduct_val or 0)
+                elif img.image_type == ImageType.back_face:
+                    face_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
+                        "back_face_deduct_score", 0)
+                    detection_face_score += float(face_reduct_val or 0)
+            except Exception as e:
+                logger.warning(f"解析 detection_json 分数失败 (image_id={img.id}): {e}")
+
+        scores["detection_score"] = detection_score
+        scores["detection_score_detail"] = {
+            "detection_center_score": detection_center_score,
+            "detection_corner_score": detection_corner_score,
+            "detection_edge_score": detection_edge_score,
+            "detection_face_score": detection_face_score,
+        }
+
+        # ---------- modified_score and is_edited status ----------
+        modified_score = 10.0
+        modified_center_score = 10.0
+        modified_corner_score = 10.0
+        modified_edge_score = 10.0
+        modified_face_score = 10.0
+
+        # 检查是否存在任何非空的 modified_json
+        is_edited = any(img.modified_json is not None for img in images)
+        scores["is_edited"] = is_edited
+
+        if is_edited:
             for img in images:
             for img in images:
+                src = img.modified_json if img.modified_json is not None else img.detection_json
                 try:
                 try:
-                    # 总分的计算
-                    add_val = img.detection_json.get("result", {}).get("_used_compute_deduct_score", 0)
-                    detection_score += float(add_val or 0)
+                    add_val = src.get("result", {}).get("_used_compute_deduct_score", 0)
+                    modified_score += float(add_val or 0)
 
 
-                    # 累加不同类型的扣分项
                     if img.image_type == ImageType.front_edge:
                     if img.image_type == ImageType.front_edge:
-                        center_reduct_val = img.detection_json.get("result", {}).get("center_result", {}).get(
-                            "deduct_score", 0)
-                        corner_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
+                        center_reduct_val = src.get("result", {}).get("center_result", {}).get("deduct_score", 0)
+                        corner_reduct_val = src.get("result", {}).get("defect_result", {}).get(
                             "front_corner_deduct_score", 0)
                             "front_corner_deduct_score", 0)
-                        edge_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
-                            "front_edge_deduct_score", 0)
-
-                        detection_center_score += float(center_reduct_val or 0)
-                        detection_corner_score += float(corner_reduct_val or 0)
-                        detection_edge_score += float(edge_reduct_val or 0)
+                        edge_reduct_val = src.get("result", {}).get("defect_result", {}).get("front_edge_deduct_score",
+                                                                                             0)
+                        modified_center_score += float(center_reduct_val or 0)
+                        modified_corner_score += float(corner_reduct_val or 0)
+                        modified_edge_score += float(edge_reduct_val or 0)
                     elif img.image_type == ImageType.back_edge:
                     elif img.image_type == ImageType.back_edge:
-                        center_reduct_val = img.detection_json.get("result", {}).get("center_result", {}).get(
-                            "deduct_score", 0
-                        )
-                        corner_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
+                        center_reduct_val = src.get("result", {}).get("center_result", {}).get("deduct_score", 0)
+                        corner_reduct_val = src.get("result", {}).get("defect_result", {}).get(
                             "back_corner_deduct_score", 0)
                             "back_corner_deduct_score", 0)
-                        edge_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
-                            "back_edge_deduct_score", 0)
-
-                        detection_center_score += float(center_reduct_val or 0)
-                        detection_corner_score += float(corner_reduct_val or 0)
-                        detection_edge_score += float(edge_reduct_val or 0)
+                        edge_reduct_val = src.get("result", {}).get("defect_result", {}).get("back_edge_deduct_score",
+                                                                                             0)
+                        modified_center_score += float(center_reduct_val or 0)
+                        modified_corner_score += float(corner_reduct_val or 0)
+                        modified_edge_score += float(edge_reduct_val or 0)
                     elif img.image_type == ImageType.front_face:
                     elif img.image_type == ImageType.front_face:
-                        face_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
-                            "front_face_deduct_score", 0)
-
-                        detection_face_score += float(face_reduct_val or 0)
+                        face_reduct_val = src.get("result", {}).get("defect_result", {}).get("front_face_deduct_score",
+                                                                                             0)
+                        modified_face_score += float(face_reduct_val or 0)
                     elif img.image_type == ImageType.back_face:
                     elif img.image_type == ImageType.back_face:
-                        face_reduct_val = img.detection_json.get("result", {}).get("defect_result", {}).get(
-                            "back_face_deduct_score", 0)
-
-                        detection_face_score += float(face_reduct_val or 0)
+                        face_reduct_val = src.get("result", {}).get("defect_result", {}).get("back_face_deduct_score",
+                                                                                             0)
+                        modified_face_score += float(face_reduct_val or 0)
 
 
                 except Exception as e:
                 except Exception as e:
-                    logger.warning(f"解析 detection_json 分数失败 (image_id={img.id}): {e}")
-            card_data["detection_score"] = detection_score
-            card_data["detection_score_detail"]["detection_center_score"] = detection_center_score
-            card_data["detection_score_detail"]["detection_corner_score"] = detection_corner_score
-            card_data["detection_score_detail"]["detection_edge_score"] = detection_edge_score
-            card_data["detection_score_detail"]["detection_face_score"] = detection_face_score
-
-            # ---------- modified_score ----------
-            modified_score = 10.0
-            modified_center_score = 10.0
-            modified_corner_score = 10.0
-            modified_edge_score = 10.0
-            modified_face_score = 10.0
-
-            all_modified_none = all(img.modified_json is None for img in images)
-            if not all_modified_none:
-
-                for img in images:
-                    src = img.modified_json if img.modified_json is not None else img.detection_json
-                    try:
-                        # 总分的计算
-                        add_val = src.get("result", {}).get("_used_compute_deduct_score", 0)
-                        modified_score += float(add_val or 0)
-
-                        # 累加不同修改后数据类型的扣分项
-                        if img.image_type == ImageType.front_edge:
-                            center_reduct_val = src.get("result", {}).get("center_result", {}).get(
-                                "deduct_score", 0)
-                            corner_reduct_val = src.get("result", {}).get("defect_result", {}).get(
-                                "front_corner_deduct_score", 0)
-                            edge_reduct_val = src.get("result", {}).get("defect_result", {}).get(
-                                "front_edge_deduct_score", 0)
-
-                            modified_center_score += float(center_reduct_val or 0)
-                            modified_corner_score += float(corner_reduct_val or 0)
-                            modified_edge_score += float(edge_reduct_val or 0)
-                        elif img.image_type == ImageType.back_edge:
-                            center_reduct_val = src.get("result", {}).get("center_result", {}).get(
-                                "deduct_score", 0
-                            )
-                            corner_reduct_val = src.get("result", {}).get("defect_result", {}).get(
-                                "back_corner_deduct_score", 0)
-                            edge_reduct_val = src.get("result", {}).get("defect_result", {}).get(
-                                "back_edge_deduct_score", 0)
-
-                            modified_center_score += float(center_reduct_val or 0)
-                            modified_corner_score += float(corner_reduct_val or 0)
-                            modified_edge_score += float(edge_reduct_val or 0)
-                        elif img.image_type == ImageType.front_face:
-                            face_reduct_val = src.get("result", {}).get("defect_result", {}).get(
-                                "front_face_deduct_score", 0)
-
-                            modified_face_score += float(face_reduct_val or 0)
-                        elif img.image_type == ImageType.back_face:
-                            face_reduct_val = src.get("result", {}).get("defect_result", {}).get(
-                                "back_face_deduct_score", 0)
-
-                            modified_face_score += float(face_reduct_val or 0)
-
-                    except Exception as e:
-                        logger.warning(f"解析 modified_json 分数失败 (image_id={img.id}): {e}")
-                card_data["modified_score"] = modified_score
-
-                card_data["modified_score_detail"]["modified_center_score"] = modified_center_score
-                card_data["modified_score_detail"]["modified_corner_score"] = modified_corner_score
-                card_data["modified_score_detail"]["modified_edge_score"] = modified_edge_score
-                card_data["modified_score_detail"]["modified_face_score"] = modified_face_score
-            else:
-                card_data["modified_score"] = None
-                card_data["modified_score_detail"]["modified_center_score"] = None
-                card_data["modified_score_detail"]["modified_corner_score"] = None
-                card_data["modified_score_detail"]["modified_edge_score"] = None
-                card_data["modified_score_detail"]["modified_face_score"] = None
+                    logger.warning(f"解析 modified_json 分数失败 (image_id={img.id}): {e}")
 
 
-        except Exception as e:
-            logger.error(f"计算分数过程异常: {e}")
+            scores["modified_score"] = modified_score
+            scores["modified_score_detail"] = {
+                "modified_center_score": modified_center_score,
+                "modified_corner_score": modified_corner_score,
+                "modified_edge_score": modified_edge_score,
+                "modified_face_score": modified_face_score,
+            }
 
 
-    # 组合成最终响应
-    card_response = CardDetailResponse.model_validate(card_data)
-    card_response.images = images
+    except Exception as e:
+        logger.error(f"计算分数过程异常: {e}")
 
 
-    return card_response
+    return scores

+ 41 - 2
app/utils/scheme.py

@@ -13,12 +13,32 @@ class ImageType(str, Enum):
     back_edge = "back_edge"
     back_edge = "back_edge"
 
 
 
 
-# 新增:用于指定要更新哪个结果图片路径的枚举
+class CardType(str, Enum):
+    pokemon = "pokemon"
+    basketball = "basketball"
+    baseball = "baseball"
+
+
+# 用于指定要更新哪个结果图片路径的枚举
 class ResultImagePathType(str, Enum):
 class ResultImagePathType(str, Enum):
     detection = "detection"
     detection = "detection"
     modified = "modified"
     modified = "modified"
 
 
 
 
+# 用于排序的
+class SortBy(str, Enum):  # NEW
+    card_name = "card_name"
+    created_at = "created_at"
+    updated_at = "updated_at"
+    detection_score = "detection_score"
+    modified_score = "modified_score"
+
+
+class SortOrder(str, Enum):  # NEW
+    asc = "ASC"
+    desc = "DESC"
+
+
 # "图片类型和计算分数分数类型映射表"
 # "图片类型和计算分数分数类型映射表"
 IMAGE_TYPE_TO_SCORE_TYPE = {
 IMAGE_TYPE_TO_SCORE_TYPE = {
     "front_face": "front_face",
     "front_face": "front_face",
@@ -66,6 +86,8 @@ class CardDetailResponse(BaseModel):
     card_name: Optional[str] = None
     card_name: Optional[str] = None
     created_at: datetime
     created_at: datetime
     updated_at: datetime
     updated_at: datetime
+    card_type: str
+    is_edited: bool
     detection_score: Optional[float] = None
     detection_score: Optional[float] = None
     modified_score: Optional[float] = None
     modified_score: Optional[float] = None
     detection_score_detail: Optional[Dict[str, Any]] = None
     detection_score_detail: Optional[Dict[str, Any]] = None
@@ -98,12 +120,29 @@ class ImageJsonPairResponse(BaseModel):
         return v
         return v
 
 
 
 
+class CardImageInListResponse(BaseModel):
+    """为卡牌列表接口定义的简化版图片响应模型"""
+    id: int
+    image_type: str
+    image_path: str
+    detection_image_path: Optional[str] = None
+    modified_image_path: Optional[str] = None
+
+    class Config:
+        from_attributes = True
+
+
 class CardListDetailResponse(BaseModel):
 class CardListDetailResponse(BaseModel):
-    """为新的卡牌列表接口定义的响应模型 (主键为 id)"""
+    """为新的卡牌列表接口定义的响应模型"""
     id: int
     id: int
     card_name: Optional[str] = None
     card_name: Optional[str] = None
+    card_type: str
+    detection_score: Optional[float] = None
+    modified_score: Optional[float] = None
+    is_edited: bool
     created_at: datetime
     created_at: datetime
     updated_at: datetime
     updated_at: datetime
+    images: List[CardImageInListResponse] = []
 
 
     class Config:
     class Config:
         from_attributes = True
         from_attributes = True