AnlaAnla 2 недель назад
Родитель
Сommit
b1be0c4d18
2 измененных файлов с 78 добавлено и 196 удалено
  1. 0 101
      Test/process_and_import_api_test.py
  2. 78 95
      app/api/auto_import.py

+ 0 - 101
Test/process_and_import_api_test.py

@@ -1,101 +0,0 @@
-import requests
-import os
-from datetime import datetime
-
-# --- 配置区域 ---
-# 替换为你的存储服务端实际地址和端口
-SERVER_URL = "http://127.0.0.1:7755"
-API_ENDPOINT = f"{SERVER_URL}/api/import/process_and_import"
-
-
-def import_card(
-        card_name: str,
-        cardNo: str,
-        card_type: str,
-        is_reflect_card: bool,
-        strict_mode: bool,
-        paths: dict
-):
-    """
-    请求后端的自动导入接口
-    """
-    # 构造请求的 JSON Payload
-    payload = {
-        "card_name": card_name,
-        "cardNo": cardNo,
-        "card_type": card_type,
-        "is_reflect_card": is_reflect_card,
-        "strict_mode": strict_mode,
-        "path_front_ring": paths.get("front_ring"),
-        "path_front_coaxial": paths.get("front_coaxial"),
-        "path_back_ring": paths.get("back_ring"),
-        "path_back_coaxial": paths.get("back_coaxial"),
-        "path_front_gray": paths.get("front_gray"),
-        "path_back_gray": paths.get("back_gray")
-    }
-
-    # 可选:在客户端进行一次基础的文件存在性校验,避免浪费网络请求
-    for key, path in payload.items():
-        if key.startswith("path_") and path is not None:
-            if not os.path.exists(path):
-                print(f"[警告] 本地文件不存在: {path}")
-                # 如果文件不存在,将其置为 None,交由服务端的 strict_mode 决定是否报错
-                payload[key] = None
-
-    print(f"\n🚀 开始请求导入接口: {card_name} (编号: {cardNo})")
-
-    try:
-        # 注意:由于后端要并发调用推理服务、转正并保存数据库,这里把 timeout 设置得稍微长一点 (120秒)
-        response = requests.post(API_ENDPOINT, json=payload, timeout=120)
-
-        if response.status_code == 200:
-            data = response.json()
-            print(f"✅ 导入成功! 返回数据: {data}")
-        else:
-            print(f"❌ 导入失败! 状态码: {response.status_code}")
-            try:
-                print(f"❌ 错误详情: {response.json()}")
-            except:
-                print(f"❌ 错误详情: {response.text}")
-
-    except requests.exceptions.Timeout:
-        print("❌ 请求超时!后端可能还在处理,请检查服务端日志。")
-    except requests.exceptions.RequestException as e:
-        print(f"❌ 请求发生异常: {e}")
-
-
-if __name__ == "__main__":
-    # --- 测试配置 ---
-    BASE_PATH = r"C:\Code\ML\Image\Card\img20_test"
-    IS_REFLECT = True
-    CARD_TYPE = "pokemon"
-
-    # 模拟循环处理 (类似你原来的逻辑)
-    for img_num in range(2, 3):  # 测试 1 到 3
-        print(f"\n>>>>> 准备处理图片组: {img_num}")
-
-        # 构造卡牌信息
-        timestamp = datetime.now().strftime('%m%d_%H%M%S')
-        card_name = f"API测试卡_{img_num}_{timestamp}"
-        cardNo = f"SN-TEST-{img_num:03d}"
-
-        # 构造路径字典 (根据你的实际命名规则修改)
-        # 注意:我在 back_ring 的地方复用了你之前脚本里硬编码的路径作为示例
-        image_paths = {
-            "front_coaxial": os.path.join(BASE_PATH, f"{img_num}_front_coaxial.jpg"),
-            "front_ring": os.path.join(BASE_PATH, f"{img_num}_front_ring.jpg"),
-            "back_coaxial": os.path.join(BASE_PATH, f"{img_num}_back_coaxial.jpg"),
-            "back_ring": r"C:\Code\ML\Image\Card\b2.jpg",  # 你原脚本中的硬编码测试路径
-            "front_gray": os.path.join(BASE_PATH, f"{img_num}_front_gray.jpg"),
-            "back_gray": os.path.join(BASE_PATH, f"{img_num}_back_gray.jpg")
-        }
-
-        # 调用封装的函数发送 HTTP 请求
-        import_card(
-            card_name=card_name,
-            cardNo=cardNo,
-            card_type=CARD_TYPE,
-            is_reflect_card=IS_REFLECT,
-            strict_mode=True,  # 严格模式:如果缺主图,服务端会直接拦截报错
-            paths=image_paths
-        )

+ 78 - 95
app/api/auto_import.py

@@ -2,10 +2,8 @@ import os
 import json
 import asyncio
 import aiohttp
-import aiofiles
-from typing import Dict, Any, Tuple, Optional
-from fastapi import APIRouter, HTTPException, Request
-from pydantic import BaseModel, Field
+from typing import Dict, Any, Tuple, Optional, Union
+from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form
 
 from app.core.config import settings
 from app.core.logger import get_logger
@@ -15,56 +13,37 @@ logger = get_logger(__name__)
 router = APIRouter()
 
 
-# 定义请求参数模型
-class AutoImportRequest(BaseModel):
-    card_name: str = Field(..., description="卡牌名称")
-    cardNo: Optional[str] = Field(None, description="卡牌编号")
-    card_type: CardType = Field(CardType.pokemon, description="卡牌类型")
-    is_reflect_card: bool = Field(True, description="是否是反光卡")
-    strict_mode: bool = Field(False, description="如果为True,必须提供所有4张主图")
-
-    front_ring: Optional[str] = Field(None, description="正面环光图绝对路径")
-    front_coaxial: Optional[str] = Field(None, description="正面同轴光图绝对路径")
-    back_ring: Optional[str] = Field(None, description="背面环光图绝对路径")
-    back_coaxial: Optional[str] = Field(None, description="背面同轴光图绝对路径")
-    front_gray: Optional[str] = Field(None, description="正面灰度图绝对路径")
-    back_gray: Optional[str] = Field(None, description="背面灰度图绝对路径")
-
-
 # --- 内部辅助函数 ---
-async def call_api_with_file(
+async def call_api_with_bytes(
         session: aiohttp.ClientSession,
         url: str,
-        file_path: str,
+        file_bytes: bytes,
+        filename: str,
         params: Dict[str, Any] = None,
         form_fields: Dict[str, Any] = None,
         file_field_name: str = 'file'
 ) -> Tuple[int, bytes]:
-    """通用的文件上传API调用函数"""
+    """通用的文件上传API调用函数 (接收字节数据)"""
     form_data = aiohttp.FormData()
 
     if form_fields:
         for key, value in form_fields.items():
             form_data.add_field(key, str(value))
 
-    if not os.path.exists(file_path):
-        raise FileNotFoundError(f"文件不存在: {file_path}")
-
-    async with aiofiles.open(file_path, 'rb') as f:
-        content = await f.read()
-        form_data.add_field(
-            file_field_name,
-            content,
-            filename=os.path.basename(file_path),
-            content_type='image/jpeg'
-        )
+    # 直接将内存中的字节数据添加到表单
+    form_data.add_field(
+        file_field_name,
+        file_bytes,
+        filename=filename or 'image.jpg',
+        content_type='image/jpeg'
+    )
 
     try:
         async with session.post(url, data=form_data, params=params) as response:
             response_content = await response.read()
             if not response.ok:
                 logger.error(
-                    f"[API Error] {url} -> Status: {response.status}, Msg: {response_content.decode('utf-8')[:100]}")
+                    f"[API Error] {url} -> Status: {response.status}, Msg: {response_content.decode('utf-8', errors='ignore')[:100]}")
             return response.status, response_content
     except Exception as e:
         logger.error(f"[Conn Error] {url} -> {e}")
@@ -73,18 +52,19 @@ async def call_api_with_file(
 
 async def process_main_image(
         session: aiohttp.ClientSession,
-        image_path: str,
+        file_bytes: bytes,
+        filename: str,
         score_type: str,
         is_reflect_card: str
 ) -> Dict[str, Any]:
     """调用推理服务,处理主图片"""
-    logger.info(f"处理主图: {score_type} -> {os.path.basename(image_path)}")
+    logger.info(f"处理主图: {score_type} -> {filename}")
     inference_base_url = settings.SCORE_UPDATE_SERVER_URL
 
     # 1. 获取转正后的图片
     rectify_url = f"{inference_base_url}/api/card_inference/card_rectify_and_center"
-    rectify_status, rectified_image_bytes = await call_api_with_file(
-        session, url=rectify_url, file_path=image_path
+    rectify_status, rectified_image_bytes = await call_api_with_bytes(
+        session, url=rectify_url, file_bytes=file_bytes, filename=filename
     )
     if rectify_status >= 300:
         raise HTTPException(status_code=500, detail=f"图片转正失败: {score_type}")
@@ -93,8 +73,8 @@ async def process_main_image(
     score_url = f"{inference_base_url}/api/card_score/score_inference"
     score_params = {"score_type": score_type, "is_reflect_card": is_reflect_card}
 
-    score_status, score_json_bytes = await call_api_with_file(
-        session, url=score_url, file_path=image_path, params=score_params
+    score_status, score_json_bytes = await call_api_with_bytes(
+        session, url=score_url, file_bytes=file_bytes, filename=filename, params=score_params
     )
     if score_status >= 300:
         raise HTTPException(status_code=500, detail=f"推理分数失败: {score_type}")
@@ -146,13 +126,13 @@ async def upload_main_image(session: aiohttp.ClientSession, base_url: str, card_
 
 
 async def upload_gray_image(session: aiohttp.ClientSession, base_url: str, card_id: int, image_type: str,
-                            file_path: str):
+                            file_bytes: bytes, filename: str):
     """将灰度图源文件上传到自身服务"""
     url = f"{base_url}{settings.API_PREFIX}/images/insert/gray/{card_id}"
     form_fields = {'image_type': image_type}
 
-    status, _ = await call_api_with_file(
-        session, url=url, file_path=file_path, form_fields=form_fields, file_field_name='image'
+    status, _ = await call_api_with_bytes(
+        session, url=url, file_bytes=file_bytes, filename=filename, form_fields=form_fields, file_field_name='image'
     )
     if status != 201:
         logger.error(f"[灰度图上传失败] {image_type} code={status}")
@@ -162,67 +142,71 @@ async def upload_gray_image(session: aiohttp.ClientSession, base_url: str, card_
 # --- 暴露的API接口 ---
 
 @router.post("/process_and_import", summary="自动化处理并导入卡牌数据")
-async def auto_import_script_api(request: Request, req_data: AutoImportRequest):
-    """
-    接收本地的图片路径,自动转正、推理分数、建立卡片关联并入库。
-参数如下:
-
-    card_name: 卡牌名称
-    cardNo: 卡牌编号
-    card_type: 卡牌类型
-    is_reflect_card: 是否是反光卡
-    strict_mode: 如果为True,必须提供所有4张主图
-    front_ring: 正面环光图绝对路径
-    front_coaxial: 正面同轴光图绝对路径
-    back_ring: 背面环光图绝对路径
-    back_coaxial: 背面同轴光图绝对路径
-    front_gray: 正面灰度图绝对路径
-    back_gray: 背面灰度图绝对路径
-    """
-    # 动态获取当前服务的 base_url (例如 http://127.0.0.1:7755)
+async def auto_import_script_api(
+        request: Request,
+        card_name: str = Form(..., description="卡牌名称"),
+        cardNo: Optional[str] = Form(None, description="卡牌编号"),
+        card_type: CardType = Form(CardType.pokemon, description="卡牌类型"),
+        is_reflect_card: bool = Form(True, description="是否是反光卡"),
+        strict_mode: bool = Form(False, description="如果为True,必须提供所有4张主图"),
+
+        front_ring: Union[UploadFile, str, None] = File(None, description="正面环光图文件"),
+        front_coaxial: Union[UploadFile, str, None] = File(None, description="正面同轴光图文件"),
+        back_ring: Union[UploadFile, str, None] = File(None, description="背面环光图文件"),
+        back_coaxial: Union[UploadFile, str, None] = File(None, description="背面同轴光图文件"),
+        front_gray: Union[UploadFile, str, None] = File(None, description="正面灰度图文件"),
+        back_gray: Union[UploadFile, str, None] = File(None, description="背面灰度图文件")
+):
     local_base_url = str(request.base_url).rstrip('/')
 
     main_inputs = {
-        "front_ring": req_data.front_ring,
-        "front_coaxial": req_data.front_coaxial,
-        "back_ring": req_data.back_ring,
-        "back_coaxial": req_data.back_coaxial
+        "front_ring": front_ring,
+        "front_coaxial": front_coaxial,
+        "back_ring": back_ring,
+        "back_coaxial": back_coaxial
     }
     gray_inputs = {
-        "front_gray": req_data.front_gray,
-        "back_gray": req_data.back_gray
+        "front_gray": front_gray,
+        "back_gray": back_gray
     }
 
-    # 1. 严格模式与路径检查
-    provided_main_count = sum(1 for p in main_inputs.values() if p is not None)
-    if req_data.strict_mode and provided_main_count != 4:
-        raise HTTPException(status_code=400,
-                            detail=f"严格模式开启,必须提供所有4张主图。当前提供了 {provided_main_count} 张。")
-    if not req_data.strict_mode and provided_main_count == 0 and not any(gray_inputs.values()):
-        raise HTTPException(status_code=400, detail="未提供任何图片,无法创建。")
+    # 【改动点2】过滤文件时,增加 isinstance(v, UploadFile) 的判断,剔除空字符串
+    valid_main_files = {
+        k: v for k, v in main_inputs.items()
+        if (v is not None) and v.filename
+    }
+    valid_gray_files = {
+        k: v for k, v in gray_inputs.items()
+        if (v is not None) and v.filename
+    }
 
-    for p in list(main_inputs.values()) + list(gray_inputs.values()):
-        if p and not os.path.exists(p):
-            raise HTTPException(status_code=400, detail=f"图片文件不存在: {p}")
+    provided_main_count = len(valid_main_files)
+    if strict_mode and provided_main_count != 4:
+        raise HTTPException(status_code=400, detail=f"严格模式开启,必须提供所有4张主图。")
+    if not strict_mode and provided_main_count == 0 and not valid_gray_files:
+        raise HTTPException(status_code=400, detail="未提供任何图片文件,无法创建。")
 
-    is_reflect_str = "true" if req_data.is_reflect_card else "false"
+    is_reflect_str = "true" if is_reflect_card else "false"
 
     async with aiohttp.ClientSession() as session:
         try:
-            # Step 1: 主图并发推理
-            logger.info(f"--- 开始自动导入任务: {req_data.card_name} ---")
-            processing_tasks = []
-            for img_type, path in main_inputs.items():
-                if path:
-                    processing_tasks.append(process_main_image(session, path, img_type, is_reflect_str))
+            # 读取所有图片至内存
+            main_bytes_data = {k: (await v.read(), v.filename) for k, v in valid_main_files.items()}
+            gray_bytes_data = {k: (await v.read(), v.filename) for k, v in valid_gray_files.items()}
 
+            # Step 1: 主图顺序推理 (防止瞬间塞爆推理服务器)
+            logger.info(f"--- 开始自动导入任务: {card_name} ---")
             processed_results = []
-            if processing_tasks:
-                processed_results = await asyncio.gather(*processing_tasks)
+            for img_type, (f_bytes, f_name) in main_bytes_data.items():
+                if len(f_bytes) == 0:
+                    raise HTTPException(status_code=400, detail=f"图片文件 {f_name} 内容为空")
+
+                res = await process_main_image(session, f_bytes, f_name, img_type, is_reflect_str)
+                processed_results.append(res)
 
-            # Step 2: 在自身数据库创建卡片记录 (携带新加的 cardNo)
+            # Step 2: 在自身数据库创建卡片记录
             card_id = await create_card_record(
-                session, local_base_url, req_data.card_name, req_data.cardNo, req_data.card_type
+                session, local_base_url, card_name, cardNo, card_type
             )
             logger.info(f"卡片记录创建成功,ID: {card_id}")
 
@@ -231,9 +215,8 @@ async def auto_import_script_api(request: Request, req_data: AutoImportRequest):
             for res in processed_results:
                 upload_tasks.append(upload_main_image(session, local_base_url, card_id, res))
 
-            for img_type, path in gray_inputs.items():
-                if path:
-                    upload_tasks.append(upload_gray_image(session, local_base_url, card_id, img_type, path))
+            for img_type, (f_bytes, f_name) in gray_bytes_data.items():
+                upload_tasks.append(upload_gray_image(session, local_base_url, card_id, img_type, f_bytes, f_name))
 
             if upload_tasks:
                 await asyncio.gather(*upload_tasks)
@@ -242,12 +225,12 @@ async def auto_import_script_api(request: Request, req_data: AutoImportRequest):
             return {
                 "message": "导入成功",
                 "card_id": card_id,
-                "card_name": req_data.card_name,
-                "cardNo": req_data.cardNo
+                "card_name": card_name,
+                "cardNo": cardNo
             }
 
         except HTTPException:
             raise
         except Exception as e:
             logger.error(f"[流程终止] 发生异常: {e}")
-            raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")
+            raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")