Browse Source

全部替换为minio url格式

AnlaAnla 2 weeks ago
parent
commit
9cad86e665
9 changed files with 290 additions and 129 deletions
  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__':
     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:
             logger.error(f"[流程终止] 发生异常: {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
 from typing import Optional, List
 from fastapi import APIRouter, Depends, HTTPException, Query
-
+from app.core.minio_client import minio_client
 from mysql.connector.pooling import PooledMySQLConnection
 
 from app.core.config import settings
@@ -198,15 +198,15 @@ def delete_card(id: int, db_conn: PooledMySQLConnection = db_dependency):
             db_conn.commit()
             logger.info(f"ID {id} 的卡牌和关联数据已成功删除。")
 
-            # 3. 删除物理文件
+            # 3. 删除物理文件 (改为 MinIO 删除)
             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} 及其所有关联数据"}
 

+ 61 - 17
app/api/images.py

@@ -2,6 +2,8 @@ import os
 import uuid
 import json
 import requests
+import io
+from app.core.minio_client import minio_client
 from typing import Optional, Dict, Any
 from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Path
 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]
     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:
-        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:
-        logger.error(f"保存图片失败: {e}")
+        logger.error(f"保存图片到MinIO失败: {e}")
         raise HTTPException(status_code=500, detail="无法保存图片文件。")
 
     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,))
         new_image_data = cursor.fetchone()
         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)))
 
+
     except IntegrityError as e:
         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:
-            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="数据库操作失败。")
 
     except Exception as e:
         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}")
         if isinstance(e, HTTPException): raise e
         raise HTTPException(status_code=500, detail="数据库操作失败。")
@@ -121,14 +148,22 @@ async def upload_gray_image_for_card(
     # 1. 保存文件
     file_extension = os.path.splitext(image.filename)[1]
     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:
-        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:
-        logger.error(f"保存灰度图片失败: {e}")
+        logger.error(f"保存图片到MinIO失败: {e}")
         raise HTTPException(status_code=500, detail="无法保存图片文件。")
 
     cursor = None
@@ -163,6 +198,7 @@ async def upload_gray_image_for_card(
         from app.crud.crud_card import EMPTY_DETECTION_JSON
         response_dict = {
             **row_dict,
+            "image_path": settings.get_full_url(row_dict.get('image_path')),
             "detection_json": EMPTY_DETECTION_JSON,  # 默认死值
             "modified_json": None,  # 刚上传还没有 modified
             "image_name": None,
@@ -173,17 +209,25 @@ async def upload_gray_image_for_card(
 
         return CardImageResponse.model_validate(response_dict)
 
+
     except IntegrityError as e:
         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:
-            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="数据库操作失败。")
 
     except Exception as e:
         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
         raise HTTPException(status_code=500, detail="数据库操作失败。")
     finally:

+ 45 - 67
app/api/rating_report.py

@@ -1,9 +1,8 @@
 from fastapi import APIRouter, HTTPException, Depends, Query
 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 PIL import Image
 
@@ -16,9 +15,6 @@ from app.utils.scheme import ImageType
 logger = get_logger(__name__)
 router = APIRouter()
 
-# 定义新的缺陷图片存储路径
-DEFECT_IMAGE_DIR = settings.DEFECT_IMAGE_DIR
-
 
 def _get_active_json(image_data: Any) -> Optional[Dict]:
     """获取有效的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:
     """
-    切割缺陷图片为正方形
-    min_rect 结构: [[center_x, center_y], [width, height], angle]
+    通过 MinIO 切割缺陷图片为正方形并保存
     """
     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 ""
 
-        with Image.open(full_path) as img:
+        # 2. 在内存中用 PIL 切图
+        with Image.open(io.BytesIO(image_bytes)) as img:
             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]
             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:
-        logger.error(f"切割图片失败: {e}")
+        logger.error(f"切割并上传图片失败: {e}")
         return ""
 
 
@@ -196,9 +174,9 @@ def generate_rating_report(
 
         # 设置主图 URL
         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":
-            response_data["backImageUrl"] = f"{settings.DATA_HOST_URL}{img.image_path}"
+            response_data["backImageUrl"] = img.image_path
 
         # 获取有效 JSON
         json_data = _get_active_json(img)
@@ -296,7 +274,7 @@ def generate_rating_report(
             "side": side,
             "location": location_str,
             "type": type_str,
-            "defectImgUrl": f"{settings.DATA_HOST_URL}{defect_img_url}"
+            "defectImgUrl": defect_img_url
         })
 
     response_data["defectDetailList"] = final_defect_list

+ 22 - 9
app/core/config.py

@@ -6,18 +6,23 @@ import json
 
 class Settings:
     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"
 
-    # 缺陷图片存储位置
-    DEFECT_IMAGE_DIR = BASE_PATH / "DefectImage"
-
     # 分数计算接口url
     SCORE_UPDATE_SERVER_URL = "http://127.0.0.1:7754"
     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.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()
-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 = []
 
-        # 处理主图片
+        # 处理主图片,补充全路径
         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))
 
         # 处理灰度图片
@@ -158,7 +161,7 @@ def get_card_with_details(db_conn: PooledMySQLConnection, card_id: int) -> Optio
                 "id": row['id'],
                 "card_id": row['card_id'],
                 "image_type": row['image_type'],
-                "image_path": row['image_path'],
+                "image_path": settings.get_full_url(row['image_path']),
                 "created_at": row['created_at'],
                 "updated_at": row['updated_at'],
                 # 虚拟字段
@@ -292,9 +295,11 @@ def get_card_list_with_images(
             for image_data in related_images:
                 image_type = image_data['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
 
@@ -399,9 +404,11 @@ def get_card_list_and_count(
                 for image_data in related_images:
                     image_type = image_data['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 {
             "total": total_count,

+ 0 - 4
app/main.py

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