|
|
@@ -0,0 +1,253 @@
|
|
|
+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 app.core.config import settings
|
|
|
+from app.core.logger import get_logger
|
|
|
+from app.utils.scheme import CardType
|
|
|
+
|
|
|
+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张主图")
|
|
|
+
|
|
|
+ path_front_ring: Optional[str] = Field(None, description="正面环光图绝对路径")
|
|
|
+ path_front_coaxial: Optional[str] = Field(None, description="正面同轴光图绝对路径")
|
|
|
+ path_back_ring: Optional[str] = Field(None, description="背面环光图绝对路径")
|
|
|
+ path_back_coaxial: Optional[str] = Field(None, description="背面同轴光图绝对路径")
|
|
|
+ path_front_gray: Optional[str] = Field(None, description="正面灰度图绝对路径")
|
|
|
+ path_back_gray: Optional[str] = Field(None, description="背面灰度图绝对路径")
|
|
|
+
|
|
|
+
|
|
|
+# --- 内部辅助函数 ---
|
|
|
+async def call_api_with_file(
|
|
|
+ session: aiohttp.ClientSession,
|
|
|
+ url: str,
|
|
|
+ file_path: str,
|
|
|
+ params: Dict[str, Any] = None,
|
|
|
+ form_fields: Dict[str, Any] = None,
|
|
|
+ file_field_name: str = 'file'
|
|
|
+) -> Tuple[int, bytes]:
|
|
|
+ """通用的文件上传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'
|
|
|
+ )
|
|
|
+
|
|
|
+ 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]}")
|
|
|
+ return response.status, response_content
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"[Conn Error] {url} -> {e}")
|
|
|
+ raise e
|
|
|
+
|
|
|
+
|
|
|
+async def process_main_image(
|
|
|
+ session: aiohttp.ClientSession,
|
|
|
+ image_path: str,
|
|
|
+ score_type: str,
|
|
|
+ is_reflect_card: str
|
|
|
+) -> Dict[str, Any]:
|
|
|
+ """调用推理服务,处理主图片"""
|
|
|
+ logger.info(f"处理主图: {score_type} -> {os.path.basename(image_path)}")
|
|
|
+ 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
|
|
|
+ )
|
|
|
+ if rectify_status >= 300:
|
|
|
+ raise HTTPException(status_code=500, detail=f"图片转正失败: {score_type}")
|
|
|
+
|
|
|
+ # 2. 获取分数JSON
|
|
|
+ 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
|
|
|
+ )
|
|
|
+ if score_status >= 300:
|
|
|
+ raise HTTPException(status_code=500, detail=f"推理分数失败: {score_type}")
|
|
|
+
|
|
|
+ return {
|
|
|
+ "image_type": score_type,
|
|
|
+ "rectified_image": rectified_image_bytes,
|
|
|
+ "score_json": json.loads(score_json_bytes)
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+async def create_card_record(session: aiohttp.ClientSession, base_url: str, card_name: str, cardNo: Optional[str],
|
|
|
+ card_type: CardType) -> int:
|
|
|
+ """调用自身服务创建新的卡牌记录"""
|
|
|
+ url = f"{base_url}{settings.API_PREFIX}/cards/created"
|
|
|
+ params = {'card_name': card_name, 'card_type': card_type.value}
|
|
|
+ if cardNo:
|
|
|
+ params['cardNo'] = cardNo
|
|
|
+
|
|
|
+ async with session.post(url, params=params) as response:
|
|
|
+ if response.status == 201:
|
|
|
+ data = await response.json()
|
|
|
+ return data.get('id')
|
|
|
+ else:
|
|
|
+ text = await response.text()
|
|
|
+ raise HTTPException(status_code=response.status, detail=f"创建卡牌记录失败: {text}")
|
|
|
+
|
|
|
+
|
|
|
+async def upload_main_image(session: aiohttp.ClientSession, base_url: str, card_id: int,
|
|
|
+ processed_data: Dict[str, Any]):
|
|
|
+ """将处理后的主图和JSON上传到自身服务"""
|
|
|
+ image_type = processed_data['image_type']
|
|
|
+ url = f"{base_url}{settings.API_PREFIX}/images/insert/{card_id}"
|
|
|
+
|
|
|
+ form_data = aiohttp.FormData()
|
|
|
+ form_data.add_field('image_type', image_type)
|
|
|
+ form_data.add_field('json_data_str', json.dumps(processed_data['score_json'], ensure_ascii=False))
|
|
|
+ form_data.add_field(
|
|
|
+ 'image',
|
|
|
+ processed_data['rectified_image'],
|
|
|
+ filename=f'{image_type}_rectified.jpg',
|
|
|
+ content_type='image/jpeg'
|
|
|
+ )
|
|
|
+
|
|
|
+ async with session.post(url, data=form_data) as response:
|
|
|
+ if response.status != 201:
|
|
|
+ logger.error(f"[主图上传失败] {image_type} code={response.status}")
|
|
|
+ raise HTTPException(status_code=500, detail=f"主图保存失败: {image_type}")
|
|
|
+
|
|
|
+
|
|
|
+async def upload_gray_image(session: aiohttp.ClientSession, base_url: str, card_id: int, image_type: str,
|
|
|
+ file_path: 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'
|
|
|
+ )
|
|
|
+ if status != 201:
|
|
|
+ logger.error(f"[灰度图上传失败] {image_type} code={status}")
|
|
|
+ raise HTTPException(status_code=500, detail=f"灰度图保存失败: {image_type}")
|
|
|
+
|
|
|
+
|
|
|
+# --- 暴露的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张主图
|
|
|
+ path_front_ring: 正面环光图绝对路径
|
|
|
+ path_front_coaxial: 正面同轴光图绝对路径
|
|
|
+ path_back_ring: 背面环光图绝对路径
|
|
|
+ path_back_coaxial: 背面同轴光图绝对路径
|
|
|
+ path_front_gray: 正面灰度图绝对路径
|
|
|
+ path_back_gray: 背面灰度图绝对路径
|
|
|
+ """
|
|
|
+ # 动态获取当前服务的 base_url (例如 http://127.0.0.1:7755)
|
|
|
+ local_base_url = str(request.base_url).rstrip('/')
|
|
|
+
|
|
|
+ main_inputs = {
|
|
|
+ "front_ring": req_data.path_front_ring,
|
|
|
+ "front_coaxial": req_data.path_front_coaxial,
|
|
|
+ "back_ring": req_data.path_back_ring,
|
|
|
+ "back_coaxial": req_data.path_back_coaxial
|
|
|
+ }
|
|
|
+ gray_inputs = {
|
|
|
+ "front_gray": req_data.path_front_gray,
|
|
|
+ "back_gray": req_data.path_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="未提供任何图片,无法创建。")
|
|
|
+
|
|
|
+ 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}")
|
|
|
+
|
|
|
+ is_reflect_str = "true" if req_data.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))
|
|
|
+
|
|
|
+ processed_results = []
|
|
|
+ if processing_tasks:
|
|
|
+ processed_results = await asyncio.gather(*processing_tasks)
|
|
|
+
|
|
|
+ # Step 2: 在自身数据库创建卡片记录 (携带新加的 cardNo)
|
|
|
+ card_id = await create_card_record(
|
|
|
+ session, local_base_url, req_data.card_name, req_data.cardNo, req_data.card_type
|
|
|
+ )
|
|
|
+ logger.info(f"卡片记录创建成功,ID: {card_id}")
|
|
|
+
|
|
|
+ # Step 3: 并发调用自身的图片保存接口
|
|
|
+ upload_tasks = []
|
|
|
+ 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))
|
|
|
+
|
|
|
+ if upload_tasks:
|
|
|
+ await asyncio.gather(*upload_tasks)
|
|
|
+
|
|
|
+ logger.info(f"--- 自动导入流程结束, Card ID: {card_id} ---")
|
|
|
+ return {
|
|
|
+ "message": "导入成功",
|
|
|
+ "card_id": card_id,
|
|
|
+ "card_name": req_data.card_name,
|
|
|
+ "cardNo": req_data.cardNo
|
|
|
+ }
|
|
|
+
|
|
|
+ except HTTPException:
|
|
|
+ raise
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"[流程终止] 发生异常: {e}")
|
|
|
+ raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")
|