فهرست منبع

全部替换为minio url格式

AnlaAnla 2 هفته پیش
والد
کامیت
9cad86e665
9فایلهای تغییر یافته به همراه290 افزوده شده و 129 حذف شده
  1. 17 15
      Test/auto_img_insert.py
  2. 111 0
      app/api/auto_import.py
  3. 9 9
      app/api/cards.py
  4. 61 17
      app/api/images.py
  5. 45 67
      app/api/rating_report.py
  6. 22 9
      app/core/config.py
  7. 10 0
      app/core/minio_client.py
  8. 15 8
      app/crud/crud_card.py
  9. 0 4
      app/main.py

+ 17 - 15
Test/auto_img_insert.py

@@ -46,20 +46,22 @@ def auto_import(data, target_url):
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
     target_url = f"http://192.168.77.249:7755/api/import/process_and_import"
     target_url = f"http://192.168.77.249:7755/api/import/process_and_import"
-    request_data = {
-        "card_name": "测试2345",
-        "cardNo": "fsg453",
-        "card_type": "pokemon",
-        "is_reflect_card": True,
-        "strict_mode": False,  # 若为 True 则必须凑齐上面定义的4张主图
-        "local_paths": {
-            "front_ring": r"C:\Code\ML\Image\Card\testimg\1_front_ring_0_1.jpg",
-            "front_coaxial": r"C:\Code\ML\Image\Card\testimg\1_front_coaxial_1_0.jpg",
-            "back_ring": r"C:\Code\ML\Image\Card\testimg\1_back_ring_0_1.jpg",
-            "back_coaxial": r"C:\Code\ML\Image\Card\testimg\1_back_coaxial_1_0.jpg",
-            "front_gray": None,
-            "back_gray": None
+
+    for i in range(2, 11):
+        request_data = {
+            "card_name": f"测试卡-{i}",
+            "cardNo": f"testNo-{i}",
+            "card_type": "pokemon",
+            "is_reflect_card": True,
+            "strict_mode": True,  # 若为 True 则必须凑齐上面定义的4张主图
+            "local_paths": {
+                "front_ring": rf"C:\Code\ML\Image\Card\img20_test\{i}_front_ring.jpg",
+                "front_coaxial": rf"C:\Code\ML\Image\Card\img20_test\{i}_back_coaxial.jpg",
+                "back_ring": r"C:\Code\ML\Image\Card\b2.jpg",
+                "back_coaxial": rf"C:\Code\ML\Image\Card\img20_test\{i}_back_coaxial.jpg",
+                "front_gray": rf"C:\Code\ML\Image\Card\img20_test\{i}_front_gray.jpg",
+                "back_gray": rf"C:\Code\ML\Image\Card\img20_test\{i}_back_gray.jpg"
+            }
         }
         }
-    }
 
 
-    auto_import(request_data, target_url)
+        auto_import(request_data, target_url)

+ 111 - 0
app/api/auto_import.py

@@ -234,3 +234,114 @@ async def auto_import_script_api(
         except Exception as e:
         except Exception as e:
             logger.error(f"[流程终止] 发生异常: {e}")
             logger.error(f"[流程终止] 发生异常: {e}")
             raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")
             raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")
+
+
+@router.post("/process_and_import_url", summary="通过URL自动化处理并导入卡牌数据")
+async def auto_import_url_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张主图URL"),
+
+        front_ring: Optional[str] = Form(None, description="正面环光图URL"),
+        front_coaxial: Optional[str] = Form(None, description="正面同轴光图URL"),
+        back_ring: Optional[str] = Form(None, description="背面环光图URL"),
+        back_coaxial: Optional[str] = Form(None, description="背面同轴光图URL"),
+        front_gray: Optional[str] = Form(None, description="正面灰度图URL"),
+        back_gray: Optional[str] = Form(None, description="背面灰度图URL")
+):
+    local_base_url = str(request.base_url).rstrip('/')
+
+    main_inputs = {
+        "front_ring": front_ring, "front_coaxial": front_coaxial,
+        "back_ring": back_ring, "back_coaxial": back_coaxial
+    }
+    gray_inputs = {
+        "front_gray": front_gray, "back_gray": back_gray
+    }
+
+    # 过滤掉空字符串
+    valid_main_urls = {k: v for k, v in main_inputs.items() if v and v.strip()}
+    valid_gray_urls = {k: v for k, v in gray_inputs.items() if v and v.strip()}
+
+    provided_main_count = len(valid_main_urls)
+    if strict_mode and provided_main_count != 4:
+        raise HTTPException(status_code=400, detail="严格模式开启,必须提供所有4张主图URL。")
+    if not strict_mode and provided_main_count == 0 and not valid_gray_urls:
+        raise HTTPException(status_code=400, detail="未提供任何图片URL,无法创建。")
+
+    is_reflect_str = "true" if is_reflect_card else "false"
+
+    async with aiohttp.ClientSession() as session:
+        try:
+            logger.info(f"--- 开始URL自动导入任务: {card_name} ---")
+
+            # 1. 并发下载图片至内存
+            async def fetch_image(img_key: str, img_url: str):
+                try:
+                    async with session.get(img_url) as resp:
+                        if resp.status != 200:
+                            raise HTTPException(status_code=400, detail=f"下载图片失败: {img_key} -> {resp.status}")
+                        file_bytes = await resp.read()
+
+                        # 尝试从 url 解析出文件名,否则使用默认名称
+                        filename = img_url.split('/')[-1].split('?')[0]
+                        if not filename or '.' not in filename:
+                            filename = f"{img_key}.jpg"
+                        return img_key, (file_bytes, filename)
+                except Exception as e:
+                    if isinstance(e, HTTPException): raise e
+                    raise HTTPException(status_code=400, detail=f"访问图片URL异常: {img_key} -> {str(e)}")
+
+            fetch_tasks = []
+            for k, url in valid_main_urls.items(): fetch_tasks.append(fetch_image(k, url))
+            for k, url in valid_gray_urls.items(): fetch_tasks.append(fetch_image(k, url))
+
+            downloaded_files = await asyncio.gather(*fetch_tasks)
+
+            # 分拣主图与灰度图的 bytes
+            main_bytes_data = {}
+            gray_bytes_data = {}
+            for key, data in downloaded_files:
+                if key in valid_main_urls:
+                    main_bytes_data[key] = data
+                else:
+                    gray_bytes_data[key] = data
+
+            # 2. 复用原有逻辑 - 主图顺序推理
+            processed_results = []
+            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)
+
+            # 3. 在自身数据库创建卡片记录
+            card_id = await create_card_record(session, local_base_url, card_name, cardNo, card_type)
+            logger.info(f"URL导入卡片记录创建成功,ID: {card_id}")
+
+            # 4. 并发调用自身的图片保存接口
+            upload_tasks = []
+            for res in processed_results:
+                upload_tasks.append(upload_main_image(session, local_base_url, card_id, res))
+            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)
+
+            logger.info(f"--- URL自动导入流程结束, Card ID: {card_id} ---")
+            return {
+                "message": "URL导入成功",
+                "card_id": card_id,
+                "card_name": card_name,
+                "cardNo": cardNo
+            }
+
+        except HTTPException:
+            raise
+        except Exception as e:
+            logger.error(f"[URL导入流程终止] 发生异常: {e}")
+            raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")

+ 9 - 9
app/api/cards.py

@@ -2,7 +2,7 @@ from datetime import datetime, date
 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 app.core.minio_client import minio_client
 from mysql.connector.pooling import PooledMySQLConnection
 from mysql.connector.pooling import PooledMySQLConnection
 
 
 from app.core.config import settings
 from app.core.config import settings
@@ -198,15 +198,15 @@ def delete_card(id: int, db_conn: PooledMySQLConnection = db_dependency):
             db_conn.commit()
             db_conn.commit()
             logger.info(f"ID {id} 的卡牌和关联数据已成功删除。")
             logger.info(f"ID {id} 的卡牌和关联数据已成功删除。")
 
 
-            # 3. 删除物理文件
+            # 3. 删除物理文件 (改为 MinIO 删除)
             for path in image_paths_to_delete:
             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}")
+                # path 通常形如 /Data/xxx.jpg 或者 /DefectImage/xxx.jpg
+                object_name = f"{settings.MINIO_BASE_PREFIX}{path}"
+                try:
+                    minio_client.remove_object(settings.MINIO_BUCKET, object_name)
+                    logger.info(f"图片文件已从MinIO删除: {object_name}")
+                except Exception as e:
+                    logger.error(f"删除MinIO文件失败 {object_name}: {e}")
 
 
             return {"message": f"成功删除卡牌 ID {id} 及其所有关联数据"}
             return {"message": f"成功删除卡牌 ID {id} 及其所有关联数据"}
 
 

+ 61 - 17
app/api/images.py

@@ -2,6 +2,8 @@ import os
 import uuid
 import uuid
 import json
 import json
 import requests
 import requests
+import io
+from app.core.minio_client import minio_client
 from typing import Optional, Dict, Any
 from typing import Optional, Dict, Any
 from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Path
 from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Path
 from fastapi.concurrency import run_in_threadpool
 from fastapi.concurrency import run_in_threadpool
@@ -45,13 +47,22 @@ async def upload_image_for_card(
 
 
     file_extension = os.path.splitext(image.filename)[1]
     file_extension = os.path.splitext(image.filename)[1]
     unique_filename = f"{uuid.uuid4()}{file_extension}"
     unique_filename = f"{uuid.uuid4()}{file_extension}"
-    image_path = settings.DATA_DIR / unique_filename
-    relative_path = f"/{image_path.parent.name}/{image_path.name}"
+
+    relative_path = f"/Data/{unique_filename}"
+    object_name = f"{settings.MINIO_BASE_PREFIX}{relative_path}"
+
     try:
     try:
-        with open(image_path, "wb") as buffer:
-            buffer.write(await image.read())
+        # 写入 MinIO 存储
+        file_bytes = await image.read()
+        minio_client.put_object(
+            settings.MINIO_BUCKET,
+            object_name,
+            io.BytesIO(file_bytes),
+            len(file_bytes),
+            content_type=image.content_type or "image/jpeg"
+        )
     except Exception as e:
     except Exception as e:
-        logger.error(f"保存图片失败: {e}")
+        logger.error(f"保存图片到MinIO失败: {e}")
         raise HTTPException(status_code=500, detail="无法保存图片文件。")
         raise HTTPException(status_code=500, detail="无法保存图片文件。")
 
 
     cursor = None
     cursor = None
@@ -84,18 +95,34 @@ async def upload_image_for_card(
         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]
+        row_dict = dict(zip(columns, new_image_data))
+
+        # 返回完整url
+        row_dict['image_path'] = settings.get_full_url(row_dict.get('image_path'))
+        row_dict['detection_image_path'] = settings.get_full_url(row_dict.get('detection_image_path'))
+        row_dict['modified_image_path'] = settings.get_full_url(row_dict.get('modified_image_path'))
         return CardImageResponse.model_validate(dict(zip(columns, new_image_data)))
         return CardImageResponse.model_validate(dict(zip(columns, new_image_data)))
 
 
+
     except IntegrityError as e:
     except IntegrityError as e:
         db_conn.rollback()
         db_conn.rollback()
-        if os.path.exists(image_path): os.remove(image_path)
+        # 清理已上传到 MinIO 的文件
+        try:
+            minio_client.remove_object(settings.MINIO_BUCKET, object_name)
+        except:
+            pass
         if e.errno == 1062:
         if e.errno == 1062:
-            raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在类型为 '{image_type.value}' 的图片。")
+            raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在...")
+
         raise HTTPException(status_code=500, detail="数据库操作失败。")
         raise HTTPException(status_code=500, detail="数据库操作失败。")
 
 
     except Exception as e:
     except Exception as e:
         db_conn.rollback()
         db_conn.rollback()
-        if os.path.exists(image_path): os.remove(image_path)
+        try:
+            minio_client.remove_object(settings.MINIO_BUCKET, object_name)
+        except:
+            pass
+
         logger.error(f"关联图片到卡牌失败: {e}")
         logger.error(f"关联图片到卡牌失败: {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="数据库操作失败。")
@@ -121,14 +148,22 @@ async def upload_gray_image_for_card(
     # 1. 保存文件
     # 1. 保存文件
     file_extension = os.path.splitext(image.filename)[1]
     file_extension = os.path.splitext(image.filename)[1]
     unique_filename = f"gray_{uuid.uuid4()}{file_extension}"  # 加个前缀区分
     unique_filename = f"gray_{uuid.uuid4()}{file_extension}"  # 加个前缀区分
-    image_path = settings.DATA_DIR / unique_filename
-    relative_path = f"/{image_path.parent.name}/{image_path.name}"
+
+    relative_path = f"/Data/{unique_filename}"
+    object_name = f"{settings.MINIO_BASE_PREFIX}{relative_path}"
 
 
     try:
     try:
-        with open(image_path, "wb") as buffer:
-            buffer.write(await image.read())
+        # 写入 MinIO 存储
+        file_bytes = await image.read()
+        minio_client.put_object(
+            settings.MINIO_BUCKET,
+            object_name,
+            io.BytesIO(file_bytes),
+            len(file_bytes),
+            content_type=image.content_type or "image/jpeg"
+        )
     except Exception as e:
     except Exception as e:
-        logger.error(f"保存灰度图片失败: {e}")
+        logger.error(f"保存图片到MinIO失败: {e}")
         raise HTTPException(status_code=500, detail="无法保存图片文件。")
         raise HTTPException(status_code=500, detail="无法保存图片文件。")
 
 
     cursor = None
     cursor = None
@@ -163,6 +198,7 @@ async def upload_gray_image_for_card(
         from app.crud.crud_card import EMPTY_DETECTION_JSON
         from app.crud.crud_card import EMPTY_DETECTION_JSON
         response_dict = {
         response_dict = {
             **row_dict,
             **row_dict,
+            "image_path": settings.get_full_url(row_dict.get('image_path')),
             "detection_json": EMPTY_DETECTION_JSON,  # 默认死值
             "detection_json": EMPTY_DETECTION_JSON,  # 默认死值
             "modified_json": None,  # 刚上传还没有 modified
             "modified_json": None,  # 刚上传还没有 modified
             "image_name": None,
             "image_name": None,
@@ -173,17 +209,25 @@ async def upload_gray_image_for_card(
 
 
         return CardImageResponse.model_validate(response_dict)
         return CardImageResponse.model_validate(response_dict)
 
 
+
     except IntegrityError as e:
     except IntegrityError as e:
         db_conn.rollback()
         db_conn.rollback()
-        if os.path.exists(image_path): os.remove(image_path)
+        # 清理已上传到 MinIO 的文件
+        try:
+            minio_client.remove_object(settings.MINIO_BUCKET, object_name)
+        except:
+            pass
         if e.errno == 1062:
         if e.errno == 1062:
-            raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在类型为 '{image_type.value}' 的图片。")
+            raise HTTPException(status_code=409, detail=f"卡牌ID {card_id} 已存在...")
         raise HTTPException(status_code=500, detail="数据库操作失败。")
         raise HTTPException(status_code=500, detail="数据库操作失败。")
 
 
     except Exception as e:
     except Exception as e:
         db_conn.rollback()
         db_conn.rollback()
-        if os.path.exists(image_path): os.remove(image_path)
-        logger.error(f"灰度图上传失败: {e}")
+        try:
+            minio_client.remove_object(settings.MINIO_BUCKET, object_name)
+        except:
+            pass
+        logger.error(f"关联图片到卡牌失败: {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:
     finally:

+ 45 - 67
app/api/rating_report.py

@@ -1,9 +1,8 @@
 from fastapi import APIRouter, HTTPException, Depends, Query
 from fastapi import APIRouter, HTTPException, Depends, Query
 from mysql.connector.pooling import PooledMySQLConnection
 from mysql.connector.pooling import PooledMySQLConnection
-import os
-import json
-import math
-from pathlib import Path
+
+import io
+from app.core.minio_client import minio_client
 from typing import List, Dict, Any, Optional
 from typing import List, Dict, Any, Optional
 from PIL import Image
 from PIL import Image
 
 
@@ -16,9 +15,6 @@ from app.utils.scheme import ImageType
 logger = get_logger(__name__)
 logger = get_logger(__name__)
 router = APIRouter()
 router = APIRouter()
 
 
-# 定义新的缺陷图片存储路径
-DEFECT_IMAGE_DIR = settings.DEFECT_IMAGE_DIR
-
 
 
 def _get_active_json(image_data: Any) -> Optional[Dict]:
 def _get_active_json(image_data: Any) -> Optional[Dict]:
     """获取有效的json数据,优先 modified_json"""
     """获取有效的json数据,优先 modified_json"""
@@ -42,75 +38,57 @@ def _get_active_json(image_data: Any) -> Optional[Dict]:
 
 
 def _crop_defect_image(original_image_path_str: str, min_rect: List, output_filename: str) -> str:
 def _crop_defect_image(original_image_path_str: str, min_rect: List, output_filename: str) -> str:
     """
     """
-    切割缺陷图片为正方形
-    min_rect 结构: [[center_x, center_y], [width, height], angle]
+    通过 MinIO 切割缺陷图片为正方形并保存
     """
     """
     try:
     try:
-        # 构建绝对路径
-        # 假设 original_image_path_str 是 "/Data/..." 格式
-        rel_path = original_image_path_str.lstrip('/\\')
-        full_path = settings.BASE_PATH / rel_path
-
-        if not full_path.exists():
-            logger.warning(f"原图不存在: {full_path}")
+        # ★ 将进来的全路径 URL 剥离为相对路径 (如 /Data/xxx.jpg) 供 MinIO 读取
+        rel_path = original_image_path_str.replace(settings.DATA_HOST_URL, "")
+        rel_path = "/" + rel_path.lstrip('/\\')
+        object_name = f"{settings.MINIO_BASE_PREFIX}{rel_path}"
+
+        # 1. 从 MinIO 获取原图字节
+        try:
+            response = minio_client.get_object(settings.MINIO_BUCKET, object_name)
+            image_bytes = response.read()
+            response.close()
+            response.release_conn()
+        except Exception as e:
+            logger.warning(f"从MinIO获取原图失败: {object_name} -> {e}")
             return ""
             return ""
 
 
-        with Image.open(full_path) as img:
+        # 2. 在内存中用 PIL 切图
+        with Image.open(io.BytesIO(image_bytes)) as img:
             img_w, img_h = img.size
             img_w, img_h = img.size
-
-            # 解析 min_rect
-            # min_rect[0] 是中心点 [x, y]
-            # min_rect[1] 是宽高 [w, h]
             center_x, center_y = min_rect[0]
             center_x, center_y = min_rect[0]
             rect_w, rect_h = min_rect[1]
             rect_w, rect_h = min_rect[1]
+            side_length = max(max(rect_w, rect_h) * 1.5, 100)
+            half_side = side_length / 2
 
 
-            # 确定裁剪的正方形边长:取宽高的最大值,并适当外扩 (例如 1.5 倍) 以展示周围环境
-            # 如果缺陷非常小,设置一个最小尺寸(例如 100px),避免切图太模糊
-            side_length = max(rect_w, rect_h) * 1.5
-            side_length = max(side_length, 100)
+            left, top = max(0, center_x - half_side), max(0, center_y - half_side)
+            right, bottom = min(img_w, center_x + half_side), min(img_h, center_y + half_side)
 
 
-            half_side = side_length / 2
+            cropped_img = img.crop((left, top, right, bottom))
+
+            # 3. 将切割后的图存入内存流,并上传到 MinIO
+            out_bytes = io.BytesIO()
+            cropped_img.save(out_bytes, format="JPEG", quality=95)
+            out_bytes.seek(0)
+
+            out_rel_path = f"/DefectImage/{output_filename}"
+            out_object_name = f"{settings.MINIO_BASE_PREFIX}{out_rel_path}"
+
+            minio_client.put_object(
+                settings.MINIO_BUCKET,
+                out_object_name,
+                out_bytes,
+                len(out_bytes.getvalue()),
+                content_type="image/jpeg"
+            )
 
 
-            # 计算裁剪框 (left, top, right, bottom)
-            left = center_x - half_side
-            top = center_y - half_side
-            right = center_x + half_side
-            bottom = center_y + half_side
-
-            # 边界检查,防止超出图片范围
-            # 如果只是想保持正方形,超出部分可以填黑,或者简单的移动框的位置
-            # 这里简单处理:如果超出边界,就移动框,实在移不动就截断
-            if left < 0:
-                right -= left  # 往右移
-                left = 0
-            if top < 0:
-                bottom -= top  # 往下移
-                top = 0
-            if right > img_w:
-                left -= (right - img_w)  # 往左移
-                right = img_w
-            if bottom > img_h:
-                top -= (bottom - img_h)  # 往上移
-                bottom = img_h
-
-            # 再次检查防止负数(比如图片本身比框还小)
-            left = max(0, left)
-            top = max(0, top)
-            right = min(img_w, right)
-            bottom = min(img_h, bottom)
-
-            crop_box = (left, top, right, bottom)
-            cropped_img = img.crop(crop_box)
-
-            # 保存
-            save_path = DEFECT_IMAGE_DIR / output_filename
-            cropped_img.save(save_path, quality=95)
-
-            # 返回 URL 路径 (相对于项目根目录的 web 路径)
-            return f"/DefectImage/{output_filename}"
+            return settings.get_full_url(out_rel_path)
 
 
     except Exception as e:
     except Exception as e:
-        logger.error(f"切割图片失败: {e}")
+        logger.error(f"切割并上传图片失败: {e}")
         return ""
         return ""
 
 
 
 
@@ -196,9 +174,9 @@ def generate_rating_report(
 
 
         # 设置主图 URL
         # 设置主图 URL
         if img_type == "front_ring":
         if img_type == "front_ring":
-            response_data["frontImageUrl"] = f"{settings.DATA_HOST_URL}{img.image_path}"
+            response_data["frontImageUrl"] = img.image_path
         elif img_type == "back_ring":
         elif img_type == "back_ring":
-            response_data["backImageUrl"] = f"{settings.DATA_HOST_URL}{img.image_path}"
+            response_data["backImageUrl"] = img.image_path
 
 
         # 获取有效 JSON
         # 获取有效 JSON
         json_data = _get_active_json(img)
         json_data = _get_active_json(img)
@@ -296,7 +274,7 @@ def generate_rating_report(
             "side": side,
             "side": side,
             "location": location_str,
             "location": location_str,
             "type": type_str,
             "type": type_str,
-            "defectImgUrl": f"{settings.DATA_HOST_URL}{defect_img_url}"
+            "defectImgUrl": defect_img_url
         })
         })
 
 
     response_data["defectDetailList"] = final_defect_list
     response_data["defectDetailList"] = final_defect_list

+ 22 - 9
app/core/config.py

@@ -6,18 +6,23 @@ import json
 
 
 class Settings:
 class Settings:
     BASE_PATH = Path(__file__).parent.parent.parent.absolute()
     BASE_PATH = Path(__file__).parent.parent.parent.absolute()
-    DATA_HOST_URL = "http://192.168.77.249:7755"
 
 
-    CONFIG_PATH = BASE_PATH / 'Config.json'
+    # --- MinIO 配置 ---
+    MINIO_ENDPOINT = "192.168.77.249:9000"
+    MINIO_ACCESS_KEY = "pZEwCGnpNN05KPnmC2Yh"
+    MINIO_SECRET_KEY = "KfJRuWiv9pVxhIMcFqbkv8hZT9SnNTZ6LPx592D4"  # 替换为你的 Secret Key
+    MINIO_SECURE = False  # 是否使用 https
+    MINIO_BUCKET = "grading"
+    MINIO_BASE_PREFIX = "score_server_data"
 
 
-    API_PREFIX: str = "/api"  # 通用前缀
+    # DATA_HOST_URL 现在直接指向 MinIO 的前缀路径
+    # (注意: 需要在 MinIO 后台将 grading 存储桶的访问权限配置为 Public 或开启特定读策略)
+    DATA_HOST_URL = f"http://{MINIO_ENDPOINT}/{MINIO_BUCKET}/{MINIO_BASE_PREFIX}"
 
 
-    DATA_DIR = BASE_PATH / "Data"
+    CONFIG_PATH = BASE_PATH / 'Config.json'
+    API_PREFIX: str = "/api"  # 通用前缀
     SCORE_CONFIG_PATH = BASE_PATH / "app/core/scoring_config.json"
     SCORE_CONFIG_PATH = BASE_PATH / "app/core/scoring_config.json"
 
 
-    # 缺陷图片存储位置
-    DEFECT_IMAGE_DIR = BASE_PATH / "DefectImage"
-
     # 分数计算接口url
     # 分数计算接口url
     SCORE_UPDATE_SERVER_URL = "http://127.0.0.1:7754"
     SCORE_UPDATE_SERVER_URL = "http://127.0.0.1:7754"
     SCORE_RECALCULATE_ENDPOINT = f"{SCORE_UPDATE_SERVER_URL}/api/card_score/score_recalculate"
     SCORE_RECALCULATE_ENDPOINT = f"{SCORE_UPDATE_SERVER_URL}/api/card_score/score_recalculate"
@@ -52,7 +57,15 @@ class Settings:
         self.DATABASE_CONFIG = config_json["mysql_config"]
         self.DATABASE_CONFIG = config_json["mysql_config"]
         self.DB_NAME = config_json["database_name"]
         self.DB_NAME = config_json["database_name"]
 
 
+    def get_full_url(self, path: str) -> str:
+        """将相对路径转换为可以直接打开的 MinIO 绝对 URL"""
+        if not path:
+            return path
+        if str(path).startswith("http"):
+            return path
+        # 移除开头的斜杠防止双斜杠 (如: /Data/xxx -> Data/xxx)
+        clean_path = str(path).lstrip("/\\")
+        return f"{self.DATA_HOST_URL}/{clean_path}"
+
 
 
 settings = Settings()
 settings = Settings()
-print(f"项目根目录: {settings.BASE_PATH}")
-print(f"数据存储目录: {settings.DATA_DIR}")

+ 10 - 0
app/core/minio_client.py

@@ -0,0 +1,10 @@
+from minio import Minio
+from app.core.config import settings
+
+# 初始化全局 MinIO 客户端
+minio_client = Minio(
+    settings.MINIO_ENDPOINT,
+    access_key=settings.MINIO_ACCESS_KEY,
+    secret_key=settings.MINIO_SECRET_KEY,
+    secure=settings.MINIO_SECURE
+)

+ 15 - 8
app/crud/crud_card.py

@@ -127,8 +127,11 @@ def get_card_with_details(db_conn: PooledMySQLConnection, card_id: int) -> Optio
 
 
         final_images_list = []
         final_images_list = []
 
 
-        # 处理主图片
+        # 处理主图片,补充全路径
         for row in main_image_records:
         for row in main_image_records:
+            row['image_path'] = settings.get_full_url(row.get('image_path'))
+            row['detection_image_path'] = settings.get_full_url(row.get('detection_image_path'))
+            row['modified_image_path'] = settings.get_full_url(row.get('modified_image_path'))
             final_images_list.append(CardImageResponse.model_validate(row))
             final_images_list.append(CardImageResponse.model_validate(row))
 
 
         # 处理灰度图片
         # 处理灰度图片
@@ -158,7 +161,7 @@ def get_card_with_details(db_conn: PooledMySQLConnection, card_id: int) -> Optio
                 "id": row['id'],
                 "id": row['id'],
                 "card_id": row['card_id'],
                 "card_id": row['card_id'],
                 "image_type": row['image_type'],
                 "image_type": row['image_type'],
-                "image_path": row['image_path'],
+                "image_path": settings.get_full_url(row['image_path']),
                 "created_at": row['created_at'],
                 "created_at": row['created_at'],
                 "updated_at": row['updated_at'],
                 "updated_at": row['updated_at'],
                 # 虚拟字段
                 # 虚拟字段
@@ -292,9 +295,11 @@ def get_card_list_with_images(
             for image_data in related_images:
             for image_data in related_images:
                 image_type = image_data['image_type']
                 image_type = image_data['image_type']
                 if image_type:
                 if image_type:
-                    card['image_path_list'][image_type] = image_data.get('image_path')
-                    card['detection_image_path_list'][image_type] = image_data.get('detection_image_path')
-                    card['modified_image_path_list'][image_type] = image_data.get('modified_image_path')
+                    card['image_path_list'][image_type] = settings.get_full_url(image_data.get('image_path'))
+                    card['detection_image_path_list'][image_type] = settings.get_full_url(
+                        image_data.get('detection_image_path'))
+                    card['modified_image_path_list'][image_type] = settings.get_full_url(
+                        image_data.get('modified_image_path'))
 
 
         return cards
         return cards
 
 
@@ -399,9 +404,11 @@ def get_card_list_and_count(
                 for image_data in related_images:
                 for image_data in related_images:
                     image_type = image_data['image_type']
                     image_type = image_data['image_type']
                     if image_type:
                     if image_type:
-                        card['image_path_list'][image_type] = image_data.get('image_path')
-                        card['detection_image_path_list'][image_type] = image_data.get('detection_image_path')
-                        card['modified_image_path_list'][image_type] = image_data.get('modified_image_path')
+                        card['image_path_list'][image_type] = settings.get_full_url(image_data.get('image_path'))
+                        card['detection_image_path_list'][image_type] = settings.get_full_url(
+                            image_data.get('detection_image_path'))
+                        card['modified_image_path_list'][image_type] = settings.get_full_url(
+                            image_data.get('modified_image_path'))
 
 
         return {
         return {
             "total": total_count,
             "total": total_count,

+ 0 - 4
app/main.py

@@ -20,8 +20,6 @@ setup_logging()
 logger = get_logger(__name__)
 logger = get_logger(__name__)
 
 
 settings.set_config()
 settings.set_config()
-os.makedirs(settings.DATA_DIR, exist_ok=True)
-os.makedirs(settings.DEFECT_IMAGE_DIR, exist_ok=True)
 
 
 
 
 @asynccontextmanager
 @asynccontextmanager
@@ -37,8 +35,6 @@ async def lifespan(main_app: FastAPI):
 
 
 app = FastAPI(title="卡片分数数据存储服务", lifespan=lifespan)
 app = FastAPI(title="卡片分数数据存储服务", lifespan=lifespan)
 
 
-app.mount("/Data", StaticFiles(directory="Data"), name="Data")
-app.mount("/DefectImage", StaticFiles(directory="DefectImage"), name="DefectImage")
 app.add_middleware(
 app.add_middleware(
     CORSMiddleware,
     CORSMiddleware,
     allow_origins=["*"],
     allow_origins=["*"],