|
|
@@ -1,7 +1,9 @@
|
|
|
import requests
|
|
|
import json
|
|
|
import copy
|
|
|
+import hashlib
|
|
|
from enum import Enum
|
|
|
+from time import perf_counter
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Body
|
|
|
from fastapi.concurrency import run_in_threadpool
|
|
|
@@ -17,7 +19,6 @@ from app.utils.scheme import (
|
|
|
)
|
|
|
from app.crud import crud_card
|
|
|
from app.utils.xy_process import convert_internal_to_xy_format, convert_xy_to_internal_format
|
|
|
-import hashlib
|
|
|
from app.core.minio_client import minio_client
|
|
|
from app.utils.rating_report_utils import crop_defect_image
|
|
|
|
|
|
@@ -142,8 +143,22 @@ def _sanitize_defects_for_recalculate(defects: list):
|
|
|
d.pop("new_score", None)
|
|
|
|
|
|
|
|
|
-def _process_defects_for_json(card_id: int, img_id: int, img_path: str, json_data: dict, side: str, all_images: list = None):
|
|
|
+def _process_defects_for_json(
|
|
|
+ card_id: int,
|
|
|
+ img_id: int,
|
|
|
+ img_path: str,
|
|
|
+ json_data: dict,
|
|
|
+ side: str,
|
|
|
+ all_images: list = None,
|
|
|
+ generate_defect_img: bool = True,
|
|
|
+ generate_related_images: bool = True
|
|
|
+):
|
|
|
+ start_time = perf_counter()
|
|
|
if not json_data or "result" not in json_data:
|
|
|
+ logger.info(
|
|
|
+ "耗时埋点 _process_defects_for_json: card_id=%s image_id=%s side=%s defects=0 elapsed_ms=%.2f",
|
|
|
+ card_id, img_id, side, (perf_counter() - start_time) * 1000
|
|
|
+ )
|
|
|
return
|
|
|
defect_result = json_data["result"].get("defect_result", {})
|
|
|
defects = defect_result.get("defects", [])
|
|
|
@@ -165,21 +180,25 @@ def _process_defects_for_json(card_id: int, img_id: int, img_path: str, json_dat
|
|
|
# 使用坐标哈希作为缓存文件名,避免重复裁剪
|
|
|
rect_str = str(min_rect)
|
|
|
rect_hash = hashlib.md5(rect_str.encode('utf-8')).hexdigest()[:8]
|
|
|
- filename = f"xy_{card_id}_{img_id}_{idx}_{rect_hash}.jpg"
|
|
|
-
|
|
|
- out_rel_path = f"/DefectImage/{filename}"
|
|
|
- out_object_name = f"{settings.MINIO_BASE_PREFIX}{out_rel_path}"
|
|
|
- try:
|
|
|
- # 检查 MinIO 中是否已有该截图,有则直接使用
|
|
|
- minio_client.stat_object(settings.MINIO_BUCKET, out_object_name)
|
|
|
- defect_img_url = settings.get_full_url(out_rel_path)
|
|
|
- except Exception:
|
|
|
- # 不存在或异常,则执行裁剪并上传
|
|
|
- defect_img_url = crop_defect_image(img_path, min_rect, filename)
|
|
|
+ if generate_defect_img:
|
|
|
+ filename = f"xy_{card_id}_{img_id}_{idx}_{rect_hash}.jpg"
|
|
|
+
|
|
|
+ out_rel_path = f"/DefectImage/{filename}"
|
|
|
+ out_object_name = f"{settings.MINIO_BASE_PREFIX}{out_rel_path}"
|
|
|
+ try:
|
|
|
+ # 检查 MinIO 中是否已有该截图,有则直接使用
|
|
|
+ minio_client.stat_object(settings.MINIO_BUCKET, out_object_name)
|
|
|
+ defect_img_url = settings.get_full_url(out_rel_path)
|
|
|
+ except Exception:
|
|
|
+ # 不存在或异常,则执行裁剪并上传
|
|
|
+ defect_img_url = crop_defect_image(img_path, min_rect, filename)
|
|
|
|
|
|
# 把同面的其他类型图在同样位置截图(不论是不是融合图都截)
|
|
|
- if all_images:
|
|
|
- same_side_images = [img for img in all_images if getattr(img, 'image_type', '').startswith(side_prefix)]
|
|
|
+ if generate_related_images and all_images:
|
|
|
+ same_side_images = [
|
|
|
+ img for img in all_images
|
|
|
+ if getattr(img, 'image_type', '').startswith(side_prefix) and getattr(img, 'id', None) != img_id
|
|
|
+ ]
|
|
|
for s_img in same_side_images:
|
|
|
s_img_type = getattr(s_img, 'image_type', '')
|
|
|
s_img_path = getattr(s_img, 'image_path', '')
|
|
|
@@ -230,13 +249,22 @@ def _process_defects_for_json(card_id: int, img_id: int, img_path: str, json_dat
|
|
|
defect_detail_list.append(detail_item)
|
|
|
|
|
|
defect_result["defectDetailList"] = defect_detail_list
|
|
|
+ logger.info(
|
|
|
+ "耗时埋点 _process_defects_for_json: card_id=%s image_id=%s side=%s defects=%s elapsed_ms=%.2f",
|
|
|
+ card_id, img_id, side, len(defects), (perf_counter() - start_time) * 1000
|
|
|
+ )
|
|
|
|
|
|
|
|
|
-def _process_images_to_xy_format(card_data: dict):
|
|
|
+def _process_images_to_xy_format(
|
|
|
+ card_data: dict,
|
|
|
+ generate_defect_img: bool = True,
|
|
|
+ generate_related_images: bool = True
|
|
|
+):
|
|
|
"""
|
|
|
内部辅助函数:遍历卡牌数据中的图片,将 JSON 格式转换为前端需要的 XY 格式。
|
|
|
直接修改传入的 card_data 字典。
|
|
|
"""
|
|
|
+ start_time = perf_counter()
|
|
|
card_id = card_data.get("id")
|
|
|
all_images = card_data.get("images", [])
|
|
|
if all_images:
|
|
|
@@ -246,7 +274,11 @@ def _process_images_to_xy_format(card_data: dict):
|
|
|
d_internal = json.loads(d_internal)
|
|
|
|
|
|
if d_internal:
|
|
|
- _process_defects_for_json(card_id, img.id, img.image_path, d_internal, img.image_type, all_images)
|
|
|
+ _process_defects_for_json(
|
|
|
+ card_id, img.id, img.image_path, d_internal, img.image_type, all_images,
|
|
|
+ generate_defect_img=generate_defect_img,
|
|
|
+ generate_related_images=generate_related_images
|
|
|
+ )
|
|
|
img.detection_json = convert_internal_to_xy_format(d_internal)
|
|
|
else:
|
|
|
img.detection_json = convert_internal_to_xy_format({})
|
|
|
@@ -256,11 +288,19 @@ def _process_images_to_xy_format(card_data: dict):
|
|
|
m_internal = json.loads(m_internal)
|
|
|
|
|
|
if m_internal:
|
|
|
- _process_defects_for_json(card_id, img.id, img.image_path, m_internal, img.image_type, all_images)
|
|
|
+ _process_defects_for_json(
|
|
|
+ card_id, img.id, img.image_path, m_internal, img.image_type, all_images,
|
|
|
+ generate_defect_img=generate_defect_img,
|
|
|
+ generate_related_images=generate_related_images
|
|
|
+ )
|
|
|
img.modified_json = convert_internal_to_xy_format(m_internal)
|
|
|
else:
|
|
|
m_fallback = copy.deepcopy(d_internal) if d_internal else {}
|
|
|
img.modified_json = convert_internal_to_xy_format(m_fallback)
|
|
|
+ logger.info(
|
|
|
+ "耗时埋点 _process_images_to_xy_format: card_id=%s image_count=%s elapsed_ms=%.2f",
|
|
|
+ card_id, len(all_images), (perf_counter() - start_time) * 1000
|
|
|
+ )
|
|
|
return card_data
|
|
|
|
|
|
|
|
|
@@ -280,6 +320,7 @@ def get_card_details(
|
|
|
"""
|
|
|
target_id = card_id
|
|
|
cursor = None
|
|
|
+ start_time = perf_counter()
|
|
|
|
|
|
try:
|
|
|
cursor = db_conn.cursor(dictionary=True)
|
|
|
@@ -298,7 +339,8 @@ def get_card_details(
|
|
|
|
|
|
if not row:
|
|
|
msg = "没有下一张" if mode == QueryMode.next else "没有上一张"
|
|
|
- raise HTTPException(status_code=200, detail=msg)
|
|
|
+ # 边界场景返回 404,避免与 response_model=CardDetailResponse 冲突
|
|
|
+ raise HTTPException(status_code=404, detail=msg)
|
|
|
|
|
|
target_id = row['id']
|
|
|
|
|
|
@@ -325,7 +367,11 @@ def get_card_details(
|
|
|
card_data['id_next'] = row_next['id'] if row_next else None
|
|
|
|
|
|
# 4. 遍历图片,转换格式 (使用抽取出的辅助函数)
|
|
|
- _process_images_to_xy_format(card_data)
|
|
|
+ _process_images_to_xy_format(
|
|
|
+ card_data,
|
|
|
+ generate_defect_img=False,
|
|
|
+ generate_related_images=True
|
|
|
+ )
|
|
|
|
|
|
# 5. 将 images 从 Pydantic 对象转为 dict,避免 model_validate 重复验证导致类型异常
|
|
|
if "images" in card_data:
|
|
|
@@ -343,6 +389,10 @@ def get_card_details(
|
|
|
logger.error(f"查询卡牌详情失败 (Mode: {mode}, BaseID: {card_id}): {e}")
|
|
|
raise HTTPException(status_code=500, detail="数据库查询失败")
|
|
|
finally:
|
|
|
+ logger.info(
|
|
|
+ "耗时埋点 get_card_details: base_card_id=%s target_card_id=%s mode=%s elapsed_ms=%.2f",
|
|
|
+ card_id, target_id, mode, (perf_counter() - start_time) * 1000
|
|
|
+ )
|
|
|
if cursor:
|
|
|
cursor.close()
|
|
|
|